1 并发
1.1 简介
Go 语言支持并发,通过 goroutines 和 channels 提供了一种简洁且高效的方式来实现并发。
1.2 Goroutine
1.2.1 简介
goroutine 协程,是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的,Go 中的并发执行单位,类似于轻量级线程。
Goroutine 的调度由 Go 运行时管理,用户无需手动分配线程,使用 go 关键字启动 Goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
Goroutine 是非阻塞的,可以高效地运行成千上万个 Goroutine。
goroutine 语法格式:go 函数名( 参数列表 )
例如:go f(x, y, z)
注意:
当一个新的Go协程启动时,协程的调用立即返回。与函数不同,程序流程不会等待Go协程结束再继续执行。程序流程在开启Go协程后立即返回并开始执行下一行代码,并忽略Go协程的任何返回值。
在主协程存在时才能运行其他协程,主协程终止则程序终止,其他协程也将终止
1.2.2 特点
goroutine特点:
可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(2MB),一个goroutine的栈在生命周期开始时只有很小的栈(2KB),goroutine的栈是不固定的,可以按需增加或者缩小,goroutine的栈大小限制可以达到1GB,虽然这种情况不多见,所以一次可以创建十万左右的goroutine是没问题的。
goroutine 调度
OS线程由OS内核来调度,goroutine则是由Go运行时(runtime)自己的调度器来调度,这个调度器使用一个m:n调度的技术(复用/调度m个goroutine到n个OS线程),goroutine的调度不需要切换内核语境,所以调用一个goroutine比调用个线程的成本要低很多。
GOMAXPROCS
Go运行时的调度使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码,默认值是机器上的CPU核心数。
例如:在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。(Go1.5版本前默认是单核心执行,Go1.5版本后默认使用全部逻辑核心数)
1.2.3 检测数据访问冲突
使用 go run -race检测数据访问冲突
比如检测 某一个内存被写的时候刚好也被另一个协程读
o run -race goroutine.go ================== WARNING: DATA RACE Read at 0x00c000138000 by main goroutine: main.main() /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:18 +0xfb Previous write at 0x00c000138000 by goroutine 7: main.main.func1() /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:13 +0x68 Goroutine 7 (running) created at: main.main() /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:11 +0xc3 ================== [11755340 5733960 10421739 12104071 7264193 14611763 1966171 6718099 11141482 2270786] Found 1 data race(s) exit status 66
1.2.4 示例
package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 5; i++ { fmt.Println("Hello") time.Sleep(100 * time.Millisecond) } } func main() { // 使用 1 个逻辑核心数跑 Go 程序 runtime.GOMAXPROCS(1) go sayHello() // 启动 Goroutine for i := 0; i < 5; i++ { fmt.Println("Main") time.Sleep(100 * time.Millisecond) } }
1.3 通道(Channel)
1.3.1 普通通道
1.3.1.1 简介
通道(Channel)是用于 Goroutine 之间的数据传递,通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯,避免了显式的锁机制。
go语言的并发模型是SCP,提倡通过通信共享内存而不是通过共享内存而实现通信
通道(channel)是引用类型,是一种特殊的类型通道像一个传送带或者队列,遵循先进先出原则,类似于队列,保证收发数据的顺序每一个通道都是一个具体类型的导管,在声明channel的时候需要为其指定元素类型
注意:对信道发送和接收数据默认是阻塞的
当数据发送到信道时,程序在发送语句处阻塞,直到其他协程从该信道中读取数据。类似地,当从信道读取数据时,程序在读取语句处被阻塞,直到其他协程向信道写入数据。
信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。
1.3.1.2 声明通道
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:ch := make(chan int)
如果使用 channel 之前没有 make,会出现 deadlock 错误
声明一个 channel 类型的变量时,实际上它是一个 nil 值。未初始化的 channel 是 nil,无法进行发送或接收操作。
写数据之前,没有其他协程阻塞接收并且没有缓冲可以存,会发生 dealock
channel make 之后没有传入数据,提取数据的时候会 deadlock
channel 满了继续传入元素会 deadlock
使用 make 函数创建一个 channel,使用 chan 关键字表示 channel,通过 <- 操作符发送和接收数据,如果未指定方向,则为双向通道(即:可读可写)
ch <- v // 把 v 发送到通道 ch v := <-ch // 从 ch 接收数据 并把值赋给 v go func(c chan int) { channel c } (a)//双向通道,读写均可的 go func(c <- chan int) { } (a) //只读的Channel go func(c chan <- int) { } (a)//只写的Channel
向 channel 传入数据, CHAN <- DATA, CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据。 从 channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里。 注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。1.3.1.3 普通通道示例
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 把 sum 发送到通道 c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从通道 c 中接收 fmt.Println(x, y, x+y) } 输出结果为: -5 17 12
1.3.2 带缓冲区通道
1.3.2.1 简介
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据,还是遵循先进先出原则
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
读取操作: 当读取一个通道时,只有通道中有值时,读取才会成功,否则会阻塞。
写入操作: 往通道写入值时,只有通道能够接收(缓冲区未满或有接收方)时,写入才会成功,否则会阻塞。
1.3.2.2 带缓冲区通道示例
package main import "fmt" func main() { // 这里我们定义了一个可以存储整数类型的带缓冲通道 // 缓冲区大小为2 ch := make(chan int, 2) // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据 // 而不用立刻需要去同步读取数据 ch <- 1 ch <- 2 // 获取这两个数据 fmt.Println(<-ch) fmt.Println(<-ch) } 执行输出结果为: 1 2
1.3.3 遍历
1.3.3.1 for 遍历
使用 v, ok := <-ch格式,如果通道接收不到数据后 ok 就为 false,相当于一直循环着等待有结果输出
package main import “fmt” var ch1 chan int func send(ch chan int) { for i:=0;i<10;i++ { ch <- i } close(ch) } func main() { ch1 = make(chan int,100) go send(ch1) // 从通道中取值 for { // 产生两个值,获取的值给 ret,是否取完的 bool 值给 ok ret,ok := <-ch1 // 判断值是否取完 if !ok { // !ok 等于 !ok == true break } fmt.Println(ret) } }
1.3.3.2 range 遍历与关闭通道
通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个 // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据之后就结束了。 //如果上面的 c 通道不关闭,那么 range 函数就不会结束,从而在接收第 11 个数据的时候就阻塞了。 for i := range c { fmt.Println(i) } }
1.3.3.3 Select
select 是 Go 中的一个控制结构,类似于 switch 语句。但是,select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行,其他不会执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
select 语句使得一个 goroutine 可以等待多个通信操作。select 会阻塞,直到其中的某个 case 可以继续执行
Go 编程语言中 select 语句的语法如下:
select { case <- channel1: // 执行的代码 case value := <- channel2: // 执行的代码 case channel3 <- value: // 执行的代码 // 可以定义任意数量的 case default: // 所有通道都没有准备好,执行的代码 }
示例一:
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(1 * time.Second) c1 <- "one" }() go func() { time.Sleep(2 * time.Second) c2 <- "two" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } } 结果为: received one received two
示例二:
package main import "fmt" func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
示例三:
package main import "fmt" func main() { var ch = make(chan int,1) for i:=0;i<10;i++ { // 解决死锁问题 select { case ch <- i: //尝试放入值,只能放入一个值,如果有值,则无法再放入 case ret := <-ch: //尝试取值,没有值拿就会出现死锁 fmt.Println(ret) } } } /* 0 2 4 6 8 */
1.3.3.4 空select
package main func main() { select { } }
我们知道select语句将会被阻塞直到其中一个case分支可执行。这个例子,select语句没有任何case分支,因此它将被永久阻塞导致死锁。程序将会触发 panic,输出如下:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [select (no cases)]: main.main() /tmp/main.go:4 +0x20