本篇笔记是笔者在第六届字节跳动青训营期间学习 Go 语言的语法笔记,主要内容为管道的阻塞机制
管道 channel 的阻塞机制
1. 机制介绍
· 管道是本身是安全的,go 中通过管道的阻塞机制来实现协程之间的同步,即通过管道彼此传递讯息。笔者自己的理解是:想象不同的协程是不同的工作者,主函数为任务分发者,分发者将任务放在一个统一的地点(即协程),由不同的协程进行认领,认领后(将数据从管道中取走)进行计算,当完成任务后,再将结果放到约定好的地点(约定好的管道,没有其他特殊要求)。若该协程无任务可以工作,需要退出,再向约定好的地点放置标志,即可退出。相关人员(主函数或者其他协程)检查后可获悉协程的工作状况。
· 在上述工作的过程中,可以发现其实在管道的视角,仅仅有写者和读者(工作者,分发者之类的概念都是由程序设计者赋予的)。又因为每个协程何时会开始工作,工作什么内容都是不可知的,因此就出现了阻塞机制。简单来讲,有以下两个层面:
-
若管道发现自己已经没有足够的空间,它会让准备向其写入的写者阻塞。直到有读者取出数据(提供空间)后,它会唤醒所有被阻塞的写者,其又可以开始进行正常工作
-
若管道发现自己已经没有数据供读者去取出,它会让准备向其取出数据的读者阻塞。直到有写者写入数据(提供数据)后,它会唤醒所有被阻塞的读者,其又可以开始进行正常工作
有趣的是,go 底层会对管道进行检测,若该管道只有写者而没有读者,或者是只有读者没有写者,其会报出死锁错误。(笔者理解这种情况没有必要使用管道,因为这种操作本身就没有意义)
2.案例设计与实现
· 下面是使用协程计算 1~n(n 在程序中为 80000)中的素数,具体代码与相关注释如下:
package main
import "fmt"
func putNum(intChan chan int, n int) {
for i := 1; i <= n; i++ {
intChan <- i
}
close(intChan)
}
func calPrime(intChan, primeChan chan int, exitChan chan bool) {
for {
num, ok := <-intChan
if !ok {
break
}
isPrime := true
for i := 2; i < num; i++ {
if num%i == 0 {
isPrime = false
break
}
}
if isPrime {
primeChan <- num
}
}
fmt.Println("this process has finished!")
exitChan <- true
}
func main() {
n, intChanSize, processNum := 80000, 2000, 4
intChan := make(chan int, intChanSize)
primeChan := make(chan int, n)
exitChan := make(chan bool, 4)
go putNum(intChan, n) // task allocator
for i := 0; i < processNum; i++ {
go calPrime(intChan, primeChan, exitChan)
}
go func() { // supervisor for process finished
for i := 0; i < processNum; i++ {
<-exitChan
}
close(exitChan)
close(primeChan) // all primes are completed
}()
cnt := 0
for {
prime, ok := <-primeChan
if !ok {
break
}
fmt.Printf("prime[%v]: %v\n", cnt, prime)
cnt++
}
}