GoRoutine协程间通信
原发于taskhub
goroutine是Golang原生支持并发的基础,也是go语言中最基本的执行单元,它具有如下的特性:
- 独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程
在使用goroutine进行并发编程时,往往会遇到协程先后、交替执行的问题,此时可使用go语言中专有的数据结构chan(管道)进行协程间的通信,其中又可分为如下几个具体情形:
1. 无缓冲--单个channel变量
func groutine3(ch1 chan bool, e chan bool) {
for i := 1; i <= POOL; i++ {
fmt.Println("A",i)
ch1 <- true
if i%2 == 1 {
fmt.Println("groutine-1:", i)
}
}
e <- true
}
func groutine4(ch1 chan bool) {
for i := 1; i <= POOL; i++ {
fmt.Println("B",i)
<-ch1
if i%2 == 0 {
fmt.Println("groutine-2:", i)
}
}
}
func main() {
ch1 := make(chan bool)
exit := make(chan bool)
go groutine3(ch1,exit)
go groutine4(ch1)
<-exit
time.Sleep(time.Second * 1)
}
- 协程3作为生产者,负责给chan写入数据,写入数据后,未立即停止并阻塞协程,而是继续向下执行;等到第二次向chan写入数据时,若发现chan的数据未被读取,则阻塞等待;
- 协程4作为消费者,需要读取chan内的数据;若在读取时发现chan为空,则阻塞协程并等待,直到chan被写入数据才继续向下执行
- 无缓存模式下,chan的数据容量默认为1,即只能传入一个数据
- channel是同步阻塞的
- 上述半开半闭方式的协程交替执行会出现先后顺序混乱
BAABBAAB
,仅考虑起始条件而忽略了终止条件 - 协程4开启->打印B->读取阻塞->协程3开启->打印A->写入chan->写入未阻塞->继续打印A->写入阻塞->协程4开启->打印B->读取chan->打印B->读取阻塞
- goroutine的执行顺序不是代码行编写的前后顺序,而是按照一定规则
2. 无缓冲--两个channel变量
func groutine1(ch1 chan bool, ch2 chan bool, e chan bool) {
for i := 1; i <= POOL; i++ {
ch1 <- true
fmt.Println("A1",i)
if i%2 == 1 {
fmt.Println("groutine-1:", i)
}
fmt.Println("A2",i)
<- ch2
fmt.Println("A3",i)
}
e <- true
}
func groutine2(ch1 chan bool, ch2 chan bool) {
for i := 1; i <= POOL; i++ {
<-ch1
fmt.Println("B1",i)
if i%2 == 0 {
fmt.Println("groutine-2:", i)
}
fmt.Println("B2",i)
ch2 <- true
fmt.Println("B3",i)
}
}
func main() {
ch1 := make(chan bool)
ch2 := make(chan bool)
exit := make(chan bool)
go groutine1(ch1,ch2,exit)
go groutine2(ch1,ch2)
<-exit
time.Sleep(time.Second * 1)
}
- 生产者-消费者模式
- 循环体首尾阻塞独占模式,两个chan交替释放控制权
- 协程1向ch1写入数据后,执行后续代码段,待到读取ch2的数据时发生阻塞,协程1阻塞等待
- 协程2读取ch1数据,执行后续代码段,然后向ch2写入数据,并再进入一次for循环,但此次被阻塞在ch1的读取阶段
- 协程2阻塞后,轮到协程1执行,协程1在等待ch2被写入数据后,开始执行后续代码
- 完成了两个协程的交替运行,且实现了并发安全
3. 缓冲模式
缓冲区可以存储10个int类型的整数,在执行生产者线程的时候,线程就不会阻塞,一次性将10个整数存入channel,在读取的时候,也是一次性读取。
package main
// 带缓冲区的channel
import (
"fmt"
"time"
)
func produce(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("Send:", i)
}
}
func consumer(ch <-chan int) {
for i := 0; i < 10; i++ {
v := <-ch
fmt.Println("Receive:", v)
}
}
func main() {
ch := make(chan int, 10)
go produce(ch)
go consumer(ch)
time.Sleep(1 * time.Second)
}
4. 总结
- 在有缓冲模式下,可以在主线程中先写后读,无需新开协程负责读取
- 在无缓冲模式下,写入数据的同时必须有协程在等待读取管道中的数据,否则将抛出死锁的错误
ch := make(chan int,1)
与ch := make(chan int)
在使用上完全不一样- 无缓冲模式是同步阻塞,有缓冲模式是异步执行