go的并发控制手段有
channel,waitgroup,context,
sync包中的rwlock,lock,pool,Once,cond,map等在另一篇文章介绍。
这篇文章将介绍这些并发控制技术的使用方式以及实现原理
目录
一:channel
二:waitgroup
底层结构
方法
方法的实现原理
waitgroup的使用
注意事项
三:context
Context 接口
Context 接口的实现结构
对外函数
使用方式
使用规范
使用示例
一:channel
对于channel的使用方式和实现原理,另一篇文章已有介绍,这里就不再多赘述。
二:waitgroup
sync.WaitGroup用来解决携程间的同步阻塞等待的问题。
可以用于一个goroutine阻塞等待n个goroutine
也可以用于n个goroutine阻塞等待1个。
或者n个阻塞等待m个。
底层结构
底层结构存储有12个字节
4字节的计数器,4字节的等待者个数,4字节是信号量
方法
waitgroup结构有3个方法:Add,Wait,Done,其中Done调用的是Add(-1)
方法的实现原理
Add方法中,根据传入参数去计算计数器,如果计数器为0,则根据等待者个数假设为n,
则调用n次释放信号量,去唤醒等待的goroutine
Wait方法中,先判断计数器为0则不等待立即返回,否则累加等待者个数后使用信号量挂起当前的goroutine、
使用方法也比较简单。
waitgroup的使用
使用方法也比较简单。
工作的携程每次开启则Add(1),执行结束则Done
阻塞等待的携程只需要调用Wait()即可。
注意事项
1.waitgroup变量在传递的过程中因为是值类型的,所以传参过程需要传递指针,
否则传参过去是一份拷贝,工作携程调用Done不会唤醒等待的携程。造成永久阻塞。
2.add和wait一定要在同一个携程里面操作。
不然如果add再另一个携程里面,wait的携程可能还没等待另一个携程add就已经退出。
三:context
Golang context是Golang的并发控制技术, 它与WaitGroup的不同点是context更容易控制派生的goroutine。
context 主要用来往子孙 goroutine 传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
通过context,我们可以方便地对同一个请求所产生地goroutine进行约束管理,可以设定超时、deadline,甚至是取消这个请求相关的所有goroutine。
上下文则几乎已经成为传递与请求同生存周期变量的标准方法。在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。
Context 接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline使用场景,子goroutinue会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
Done方法返回一个信道(channel),即它是一个表示Context是否已关闭的信号
当Done信道关闭后,Err方法表明Context被撤的原因。
Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。
Context 接口的实现结构
emptycontext 实现,通常用作父的context
cancelCtx 实现,父goroutine主动调用cancel函数取消子goroutinue
timerCtx ,父goroutine主动取消,或者定时或指定时间调用cancel函数取消子goroutinue
valueCtx ,父goroutinue将值传给子goroutinue
对外函数
//go 的context对外提供6个函数
func Background() Context
func TODO() Context
// 返回 emptyCtx
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 返回cancelCtx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout函数内也是调用WithDeadline
// 返回timerCtx
func WithValue(parent Context, key, val interface{}) Context
// 返回valueCtx
使用方式
Goroutine,他们的创建和调用关系总是像层层调用进行的,就像人的辈分一样,而更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(不然程序可能就失控了)。为了实现这种关系,Context结构也应该像一棵树,叶子节点须总是由根节点衍生出来的。
要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点Background()它常常作为处理Request的顶层context存在。
有了根节点,又该怎么创建其它的子节点,孙节点呢?context包为我们提供了多个函数来创建他们:
使用规范
1.不要把context存储在结构体中,而是要显式地进行传递
2.把context作为第一个参数,并且一般都把变量命名为ctx
3.就算是程序允许,也不要传入一个nil的context,如果不知道是否要用context的话,用context.TODO()来替代
4.context.WithValue()只用来传递请求范围的值,不要用它来传递可选参数
5.就算是被多个不同的goroutine使用,context也是安全的
使用示例
valueCtx
这里子携程中一直使用ctx中的值处理逻辑
或者等待取消结束运行,这里不会结束,因为valueCtx没有取消函数,
一般都和别的组合使用。
package main
import (
"fmt"
"time"
"context"
)
func HandelRequest(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done.")
return
default:
fmt.Println("HandelRequest running, parameter: ", ctx.Value("parameter"))
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx := context.WithValue(context.Background(), "parameter", "1")
go HandelRequest(ctx)
time.Sleep(10 * time.Second)
}