Go 语言并发简单总结(一)
——《go语言并发之道》
约束
- 特殊限制
通过公约实现约束,例如团队规章或者代码库规范设置 - 词法约束
将数据的读写处理暴露给需要它的并发进程,例如,使用使用闭包将某个channel的写入操作约束起来。
for-select循环
for{//无限循环或者使用range
select{
//使用channel进行作业
}
}
建议的使用场景:
- 向channel发送迭代变量
- 循环等待停止
防止goroutine泄露
为了能够及时地通知一个goroutine完成或取消工作,使用一个信号通道进行管理。在父子之间建立一个只读的channel,父goroutine将该channel传递给子goroutine,然后在想取消子goroutine时,关闭该channel
doWork := func(
done <-chan interface{},
){
go func(){
for{//这里又用到了select循环模式
select{
case <-done:
//done被关闭时,子goroutine结束工作
return
case:
//正常处理工作
}
}
}
}
错误处理
type Result struct{
Error error,
//other data or info,
}
通过构建一个包含错误信息的结构体,并在并发的子goroutine结束时返回该类型变量,在父进程中检查error来处理错误
pipeline
属性定义:
- 一个输入的参数与返回值类型相同的stage
- 一个stage必须通过编程语言实现之后才能被当作参数传递
对于Pipeline来说,处理方式分为流处理和批处理,对比如下:
流处理
multiply := func(value, multilier int) int{
return multilier * value
}
使用流处理,对于内存占用空间消耗相对少,但是,不得不将pipeline放到循环结构中去,这限制了我们向重复利用的pipeline发送消息,而且对于程序的扩展性也不强。同时,尽管函数调用的代价很低,但是循环的每次迭代进行多次调用,并发性将受到威胁。
批处理
multiply := func(value []int, multilier int) int{
multipliedValues := make([]int, len(values))
for i,v := range values{
multipliedValues = values[i] * multilier
}
return multipliedValues
}
扇入扇出
考虑在多个goroutine上重用pipeline的单个stage以试图并行化来自上游的stage 的pull,这有助于提高pipeline的性能
在以下两个条件都成立的情况下,考虑使用扇出模式:
- 不依赖于之前stage计算出的值
- 运行需要很长时间
例如:
numFinders := runtime.NumCPU()
finders := make([]<-chan int, numFinders)
for i:= 0; i<numFinders; i++{
finders[i] = primeFinder(done,randIntStream)
}
primeFinder是素数筛选函数
启动该stage的多个副本,这里副本数量由CPU核心数决定,实际使用过程中,可以采用一些经验性的测试确定CPU的最佳数量。
现在,我们开启了四个子goroutine,对应的输出了四个不同的channel,为了综合结果,使用扇入模式,将多个数据流复用或者合并成一个流
fanIn := func(
done <-chan interface{},
channels ...<-chan interface{},
) <-chan interface{}{
var wg sync.WaitGroup
multiplexedStream := make(chan interface{})
multiplex := func(c<-chan interface){
defer wg.Done()
for i:=range c{
select{
case <-done:
return
case multiplexedStream <-i:
}
}
}
wg.Add(len(channels))
for _,c:=range channels{
go multiplex(c)
}
go func(){
wg.Wait()
close(multiplexedStream)
}()
return multiplexedStream
}
or-done-channel
操作来自系统各个部分的channel时,无法做出断言,因此得进行一些判断,在数量较多时,将会带来不必要的工作,此时,可以用一个goroutine解决这个问题,将我们关心的数据重点暴露出来,封装其他不必要部分。
orDone := func(done, c<- chan interface{}) <-chan interface{}{
varStream := make(chan interface{})
go func(){
defer close(varStream)
for{
select{
case <-done:
return
case var,ok := <-c:
if ok == false{
return
}
select{
case valStream<-c:
case <-done
}
}
}()
return valStream
}
for val := range orDone(done, myChan){
//执行操作
}
tee-channel
传递一个读channel,返回两个相同值的单独的channel
实现略
队列排队
队列几乎不会加速程序运行的总时间,它只能让程序的行为有所不同
证明:
根据利特尔法则:
其中,L是系统中平均负载数, 是负载的平均到达率,W是负载在系统中花费的平均时间。
如果我们在系统中增加队列,本质上是在增加L。那么要么是增大了——,要么是增大了W——,并不会减少负载在系统中的花费时间。
但是,利特尔法则不能预知的情况是处理请求的失败。某些情况下,队列的存在是必要的。