一、设计需求
GoLang通过开启多个Goruntine来实现并发,典型的是在网络编程中,无论是RPC调用还是web服务,当Sever端收到一个网络请求request后,都将开启一个对应的Goruntine进行处理,由于服务需求的不同,有可能需要多个不同的Goroutine,出于并发效率和异步的考虑,这些Goroutine又有可能开启多个子routine进行工作,因此往往一个Request请求都将在Server端开启一个routine树,这时,对于routine树的管理工作变得至关重要:
- 多个Goroutine之间需要安全地共享Request中包含的请求元数据
- 父routine对于子routine应该有充分的控制权,可以进行统一的消息传递和关闭操作
- 如果对应的request被取消或者timeout,就需要所有为这个request服务的gorountine被快速回收。
上述工作需要设计并发控制来完成,在GoLang中,能够实现并发控制的主要方法有:
1、开启专用的通道进行消息传递(chan 通知)
package main
import (
"fmt"
"time"
)
func main(){
ch1:=make(chan bool)
ch2:=make(chan bool)
go Worker1(ch1)
go Worker2(ch2)
time.Sleep(3*1e9)
ch1<-true
time.Sleep(3*1e9)
ch2<-true
}
func Worker1(ch chan bool){
for ;;{
select{
case <-ch:{
fmt.Println("Worker1: I was cancled by parent goruntinue")
return
}
default:
time.Sleep(1e9)
fmt.Println("Worker1: I am doing my work")
}
}
}
func Worker2(ch chan bool){
for ;;{
select{
case <-ch:{
fmt.Println("Worker2: I was cancled by parent goruntinue")
return
}
default:
time.Sleep(1e9)
fmt.Println("Worker2: I am doing my work")
}
}
}
可以看到,父routine(main)为子routine(worker1\worker2)分配了专用的通道(ch1\ch2)专用于并发控制,在子routine中,使用select语句判断父线程是否通过通道发送了关闭通知,否则将继续进行自己的工作,而父routine通过向chan发送一个值关闭子routine,这种方式虽然有效实现了控制操作,但存在很多问题:
- 通过向某chan发送一值的方式关闭子routine,逻辑上不够直观,造成代码晦涩难懂。
- 对于一个父routine开启多个子routine的情况,子routine可能需要同步或异步的进行回收,需要维护多个chan值。
- 对于复杂的嵌套开启routine的场合,这种方法显得更加捉襟见肘,比如A开启B,向B设置了一个控制用通道ch1,B开启C,此时如果B想对C进行控制,需要建立一个新的通道ch2,但若只将ch2交给C,A将失去对C的控制,势必需要添加额外的逻辑段,可以想象,对于一个复杂的routine树,这种方式将变得复杂和晦涩。
而通过chan进行消息共享往往也是行不通的:
package main
import (
"fmt"
"time"
)
func main(){
ch:=make(chan int)
go func()(){
for ;;{
fmt.Println("Goroutine1:",<-ch)
}
}()
go func()(){
for ;;{
fmt.Println("Goroutine2:",<-ch)
}
}()
ch<-1
ch<-1
time.Sleep(5*1e9)
}
在以上的代码中,main同时开启两个routine工作,通同一个通道ch尝试向两个通道发送消息,运行可以看到:
- 向同一个chan发送的消息,每次仅有一个routine可