进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。
多线程程序在单核心的 cpu 上运行,称为并发;多线程程序在多核心的 cpu 上运行,称为并行。
并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
goroutine 是一种非常轻量级的实现,可在单个进程里执行成千上万的并发任务,它是Go语言并发设计的核心。
使用 go 关键字就可以创建 goroutine,将 go 声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 goroutine。
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。
方法一 : 在调用方法时
go 函数名( 参数列表 )
使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。
方法二:
//新建一个匿名方法并执行
go func(param1, param2) {
}(val1, val2)
方法三:
//直接新建一个 goroutine 并在 goroutine 中执行代码块
go {
//do someting...
}
如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。
开启子线程 每隔1秒输出 hello world,输出10次,在主线程同时每隔1秒输出hello golang,输出10次
package main
import (
"fmt"
"strconv"
"time"
)
func main() {
// 开启协程 子线程
go printHelloWorld()
for i := 1; i <= 10; i++ {
fmt.Println("主线程 hello golang " + strconv.Itoa(i))
// 休眠1秒
time.Sleep(time.Second)
}
}
// 每个1秒输出hello world
func printHelloWorld(){
for i := 1; i <= 10; i++ {
fmt.Println("子线程 hello world " + strconv.Itoa(i))
// 休眠1秒
time.Sleep(time.Second)
}
}
如果主线程输出 5 次 hello golang结果会怎么样呢
输出结果如下
当主线程退出时,协程即使没有执行完毕,也会退出。当然,协程也可以在主线程结束前自己退出。
Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到设置CPU数目
runtime.GOMAXPROCS(逻辑CPU数量)
这里的逻辑CPU数量可以有如下几种数值:
<1:不修改任何数值。
=1:单核心执行。
>1:多核并发执行。
可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置
runtime.GOMAXPROCS(runtime.NumCPU())
如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
声明语法格式如下:
var 变量名称 chan 管道类型
注意:
管道实例 := make(chan 数据类型)
package main
import "fmt"
func main() {
var intChan chan int
intChan = make(chan int,3)// 初始管道容量3
//注意,写入数据时,不要超过初始容量,否则会报错
intChan <- 3
intChan <- 4
fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan))
//从管道中接收数据
var num int
num = <- intChan
fmt.Println("num = ",num) //
fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan))
// 在没有使用协程的情况下,将channel接收完毕后,再接收就会报 all goroutines are asleep - deadlock!
}
使用内置函数 close 可以关闭 channel, 当 channel 关闭后, 就不能再向 channel 写数据了, 但是仍然可以从该 channel 读取数据。
channel 支持 for–range 的方式进行遍历, 请注意两个细节
intChan2 := make(chan int,100)
for i := 0; i < 100; i++ {
intChan2 <- i * 2
}
// 遍历时,channel没有关闭,就会报 all goroutines are asleep - deadlock!
close(intChan2)
for data := range intChan2 {
fmt.Println("v = ",data)
}
向管道写入数据
通道的发送使用特殊的操作符 <-
格式如下
通道变量 <- 值
这里的值可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。
var intChan chan int
intChan = make(chan int,3)// 初始管道容量3
//注意,写入数据时,不要超过初始容量,否则会报错
intChan <- 3
intChan <- 4
fmt.Printf("channel 长度 = %v,容量 = %v\n",len(intChan),cap(intChan)) //长度 = 2,容量 = 3
通道接收同样使用<-操作符,通道接收有如下特性:
通道的收发操作在不同的两个 goroutine 间进行。
由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。
接收将持续阻塞直到发送方发送数据。
如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
通道一次只能接收一个数据元素。
通道的数据接收一共有以下 4 种写法。
阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量
非阻塞接收数据
data, ok := <-ch
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel 进行。
<-ch
使用通道做并发同步的写法,可以参考下面的例子:
package main
import (
"fmt"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
fmt.Println("start goroutine")
// 通过通道通知main的goroutine
ch <- 0
fmt.Println("exit goroutine")
}()
fmt.Println("wait goroutine")
// 等待匿名goroutine
<-ch
fmt.Println("all done")
}
输出如下:
wait goroutine
start goroutine
exit goroutine
all done
for data := range ch {
}
通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。
package main
import (
"fmt"
"time"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
// 从3循环到0
for i := 3; i >= 0; i-- {
// 发送3到0之间的数值
ch <- i
// 每次发送完时等待
time.Sleep(time.Second)
}
}()
// 遍历接收通道数据
for data := range ch {
// 打印通道数据
fmt.Println(data)
// 当遇到数据0时, 退出接收循环
if data == 0 {
break
}
}
}
输出如下:
3
2
1
0
案例演示:
开启一个写协程,向管道写入50个整数,再开启一个读协程,接收这50个整数。写协程和读协程操作的是同一个管道。主线程main需要等写协程和读协程都完毕之后才能退出。
package main
import "fmt"
func main() {
intChan := make(chan int)
exitChan:= make(chan bool)
go writeData(intChan)
go readData(intChan,exitChan)
<- exitChan //阻塞 直到exitChan写入
}
func writeData(intChan chan int){
for i := 1; i <= 50; i++ {
intChan <- i//写入数据
}
close(intChan)//关闭管道
}
func readData(intChan chan int,exitChan chan bool){
for {
v,ok := <- intChan
if !ok { //如果没有读取到就退出循环
break
}
fmt.Printf("readData 读取到数据 %v \n",v)
}
exitChan <- true //告诉main 已经读取完毕,可以退出了
}
案例2:
统计1至8000的素数,
package main
import "fmt"
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
// 开启协程,写入8000个整数
go putNNum(intChan)
// 开启4个线程,从intChan取出整数并判断是否为素数,是素数就放入到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan,primeChan,exitChan)
}
go func() {
for i := 0; i < 4; i++ {
<- exitChan //main 阻塞状态,直到exitChan写入了4个数据
}
close(primeChan)
}()
// 遍历primeChan,取出所有的素数
for {
res, ok := <- primeChan
if !ok {
break
}
fmt.Printf("素数 = %v\n",res)
}
fmt.Println("主main退出")
}
// 向管道写入 1---8000的整数
func putNNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool) {
//var num int
var isPrime bool
for {
num,ok := <- intChan
if !ok {
break
}
//判断 num 是不是素数
isPrime = true
for i := 2; i < num; i++ {
if num % i == 0 { // num 不是素数
isPrime = false
break
}
}
if isPrime {
// 将这个素数放入到primeChan
primeChan <- num
}
}
fmt.Println("本primeNum协程已完成工作")
exitChan <- true
}
var 通道实例 chan<- 元素类型 // 只能写入数据的通道
var 通道实例 <-chan 元素类型 // 只能读取数据的通道
与 switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
代码演示
package main
import (
"fmt"
"time"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
//问题,在实际开发中,可能我们不好确定什么关闭该管道.
//可以使用select 方式可以解决
//label:
for {
select {
//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//,会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
演示
package main
import (
"fmt"
"time"
)
//函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func test() {
//这里我们可以使用defer + recover
defer func() {
//捕获test抛出的panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0] = "golang" //map需要先make才能使用,所以在这里会报错
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}