Go并发控制
并发控制
"多线程"编程的最重要的两个点就是,"线程"间通信以及并发控制。(这里线程泛指各自独立运行的code)
什么是并发控制呢?线程在各自运行的时候由于他们之间是隔离的,对于它们执行到哪一步我们是无感的,但是在一些场合下,我们需要对某些线程的执行进行控制,比如关闭线程,暂停线程,这些经典的并发场景,每个语言都有自己的解决方案,比如用信号量来控制进程。在Go语言中,我们可以通过几种方式来做并发控制,channel , waitGroup(实质上就是CAS实现的信号量),contex , Mutex。这篇文章主要讲述前三者的使用和原理实现,关于go中Mutex的实现,以及更底层的信号量的实现可以看另外一篇文章。
利用channel来实现并发控制
channel是Go解决并发问题的首推之选,由于它的调度机制,以及内部recq,sendq的实现,可以减少锁的时间,缺点就是我们可能并不习惯这种方式,可能写起代码来不够"优雅"。
场景一
1.子协程通知父协程自己完成工作
2.父协程关闭子协程(这里还是存在一定的时间问题,和case执行的语句的时间有关,如何将控制的精度更好呢?)
利用channel来进行父子进程的通信,从而实现并发控制
//子协程通知父协程
func Worker(id int,ch chan int) {
// Do some work
fmt.Printf("worker %d work\n",id)
time.Sleep(time.Second * 2)
ch<- 8
}
func main() {
channels := make([]chan int,10)
for i:=0;i<10;i++ {
ch := make(chan int)
channels[i] = ch
go Worker(i,ch)
}
for i,ch := range channels {
<-ch
fmt.Printf("worker %d finished work \n",i)
}
}
//父协程通知子协程
func Worker(id int,ch chan int) {
// Do some work
for {
select{
case <-ch:
// 父协程控制退出
fmt.Printf("worker %d exit\n",id)
return
default:
fmt.Printf("worker %d do work\n",id)
time.Sleep(time.Second)
}
}
}
func main() {
channels := make([]chan int,10)
for i:=0;i<10;i++ {
ch := make(chan int)
channels[i] = ch
go Worker(i,ch)
}
time.Sleep(5)
// 发起信号让其退出
for i:=0;i<10;i++ {
channels[i]<-1
}
}
场景二
父协程暂停子协程
//父协程通知子协程
func Worker(id int,ch chan chan int) {
// Do some work
for {
select{
case nch := <-ch:
opt := <-nch
if opt == -1 {
// 父协程控制退出
fmt.Printf("worker %d exit\n",id)
return
}
default:
fmt.Printf("worker %d do work\n",id)
time.Sleep(time.Second)
}
}
}
func main() {
channels := make([]chan chan int,10)
for i:=0;i<10;i++ {
ch := make(chan chan int)
channels[i] = ch
go Worker(i,ch)
}
// time.Sleep(time.Second)
// 暂停子协程
stops := make([]chan int,10)
for i:=0;i<10;i++ {
stops[i] = make(chan int)
channels[i] <- stops[i]
}
time.Sleep(5*time.Second)
// 传入-1关闭协程
for i:=0;i<10;i++ {
stops[i] <- -1
}
time.Sleep(time.Second)
}
WaitGroup
wg非常适合做父协程等待子协程完成工作之类的事情,
注意:wg不是引用类型传递的时候要传递指针,不然就死锁咯。
场景一
//父协程通知子协程
func Worker(wg *sync.WaitGroup) {
// do some work
time.Sleep(time.Second)
wg.Done()
}
func main() {
wg := &sync.WaitGroup{}
wg.Add(5)
for i:=0;i<5;i++ {
go Worker(wg)
}
wg.Wait()
}
源码分析
可以在sync/waitgroup.go 中找到它的源码
type WaitGroup struct {
noCopy noCopy
//为什么要这么分配是很有趣的,相关知识字节对齐
//因为一般编译工具都是32位的,但是atomic操作要求操作数是64位的,所以这里将counter waiter合在一个64bits里,用高低位来表示。
//但是32位的编译器不能做到64位对齐,所以这里加了个32位的信号量---想想为什么?想不明白的话可以看下面的函数
//用int32 atomic不行,用int64又浪费内存...恼火...
state1 [3]uint32
}
nocopy
这里有一个装饰接口,由于wg传值的死锁问题,我们可以使用go vet 对代码进行语法检查,如果我们对noCopy的对象进行值传递,则会提醒我们
PS D:\Dev\goDev\gopath\src\goLearn> go vet main.go
# command-line-arguments
.\main.go:9:16: Worker passes lock by value: sync.WaitGroup contains sync.noCopy
.\main.go:19:19: call of Worker copies lock value: sync.WaitGroup contains sync.noCopy
state1
state1总共包含三个变量
-
counter: 还没有结束的执行者数量 add down就是对其进行操作
-
waiter count: 等待者数量
-
semaphore: 信号量--用来唤醒等待者
为什么需要sema这个32位的原因
// 返回指向state(counter+waiter )的指针和信号量的指针
// 总是将
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
Add
func (wg *WaitGroup) Add(delta int) {
statep, semap := wg.state()
state := atomic.AddUint64(statep, uint64(delta)<<32)
v := int32(state >> 32)
w := uint32(state)
if v > 0 || w == 0 {
return
}
*statep = 0
// v<=0 并且有等待者 释放信号量
for ; w != 0; w-- {
runtime_Semrelease(semap, false, 0)
}
}
Wait
// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
statep, semap := wg.state()
//代码中经常出现的这些race.Enabled 是用来做竞态检测的
//比如多个人对某个数据进行+ 如果没有竞态检测,就有可能造成结果异常
//可以在go 命令后添加 -race 启用静态检测
if race.Enabled {
_ = *statep // trigger nil deref early
race.Disable()
}
for {
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
if v == 0 {
return
}
// Increment waiters count.
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(semap)
return
}
}
}
如何对代码进行竞态检测
#竞态检测
lin@DESKTOP-VO5AF7F:/mnt/d/Dev/goDev/gopath/src/goLearn$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x0000005f8ff8 by goroutine 10:
main.Worker()
/mnt/d/Dev/goDev/gopath/src/goLearn/main.go:13 +0x47
Previous write at 0x0000005f8ff8 by goroutine 7:
main.Worker()
/mnt/d/Dev/goDev/gopath/src/goLearn/main.go:13 +0x63
Goroutine 10 (running) created at:
main.main()
/mnt/d/Dev/goDev/gopath/src/goLearn/main.go:22 +0xab
Goroutine 7 (finished) created at:
main.main()
/mnt/d/Dev/goDev/gopath/src/goLearn/main.go:22 +0xab
==================
Done
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
Context
Context在父子层级较深的并发控制问题上,很好用。
可以在ctx构成的树上传递信号以及数据

接口定义
type Context interface {
//返回这个Context设置的deadline时间--如果没有设置那么ok为false--可以重复调用
Deadline() (deadline time.Time, ok bool)
//返回一个只读channel
//当ctx关闭后--这个channel会被关闭(channnel关闭变成可读状态)
Done() <-chan struct{}
//如果context 结束 那么返回context结束的原因--可以重复调用
Err() error
//可以保存键值对
Value(key interface{}) interface{}
}
emptyCtx 一般作为ctx的根节点,方法返回nil
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
var (
//两者只是语义上的不同
background = new(emptyCtx)
todo = new(emptyCtx)
)
cancelCtx
type cancelCtx struct {
//父节点
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // 保存子节点
err error // set to non-nil by the first cancel call
}
当前canceler被取消的时候,他会取消所有的它的子节点
Done
就是懒加载初始化chan
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
cancel
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
//设置关闭原因
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
//将自己从父节点移除
if removeFromParent {
removeChild(c.Context, c)
}
}
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
propagateCancel
将自己挂载在合适的父节点上,以满足父节点取消它也会取消的特点
如果没有合适的父节点那么采用一个协程来监控父节点是否关闭
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
使用场景
func HandleRequest(ctx context.Context) {
// 可能完成这个请求需要同时进行redis和db请求
go HandleRedis(ctx)
go HandleDB(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("HandleRequest Done!")
return
default:
fmt.Println("HandleRequest Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleRedis Done!")
return
default:
fmt.Println("HandleRedis Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleDB Done!")
return
default:
fmt.Println("HandleDB Work!")
time.Sleep(2*time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go HandleRequest(ctx)
time.Sleep(5 * time.Second)
println("stop all things")
cancel()
time.Sleep(3*time.Second)
}
TimerCtx
给CancelCtx一个增加了time.Timer(可以定时执行某个动作) 来定时取消ctx
如果父节点超时了所有子节点也应该超时--实现和cancel里的差不多
WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
//到期时间
return WithDeadline(parent, time.Now().Add(timeout))
}
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 父节点
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
//设置超时自动取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
使用场景
func HandleRequest(ctx context.Context) {
// 可能完成这个请求需要同时进行redis和db请求
go HandleRedis(ctx)
go HandleDB(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("HandleRequest Done!")
return
default:
fmt.Println("HandleRequest Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleRedis Done!")
return
default:
fmt.Println("HandleRedis Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleDB Done!")
return
default:
fmt.Println("HandleDB Work!")
time.Sleep(2*time.Second)
}
}
}
func main() {
ctx, _ := context.WithTimeout(context.Background(),time.Second*5)
go HandleRequest(ctx)
time.Sleep(7*time.Second)
}func HandleRequest(ctx context.Context) {
// 可能完成这个请求需要同时进行redis和db请求
go HandleRedis(ctx)
go HandleDB(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("HandleRequest Done!")
return
default:
fmt.Println("HandleRequest Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleRedis Done!")
return
default:
fmt.Println("HandleRedis Work!")
time.Sleep(2*time.Second)
}
}
}
func HandleDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandleDB Done!")
return
default:
fmt.Println("HandleDB Work!")
time.Sleep(2*time.Second)
}
}
}
func main() {
ctx, _ := context.WithTimeout(context.Background(),time.Second*5)
go HandleRequest(ctx)
time.Sleep(7*time.Second)
}
valueCtx
就是一个普通的Ctx可以适合用来给所有字节点来传递信息
type valueCtx struct {
Context
key, val interface{}
}
Value
如果找不到就继续向父节点寻找
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
Context的使用实践
context的使用可以很灵活我们可以CancelCtx和valueCtx..
参考文章
《Go专家编程》

浙公网安备 33010602011771号