Loading

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总共包含三个变量

  1. counter: 还没有结束的执行者数量 add down就是对其进行操作

  2. waiter count: 等待者数量

  3. 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专家编程》

https://juejin.cn/post/6844904070667321357

posted @ 2021-10-23 16:22  Gopher%Lin  阅读(302)  评论(0)    收藏  举报