Go 协程控制

Deadlock

在 go 协程使用中很容易出现 deadlock 的错误,这种错误的本质是因为 go 认为当前程序中所有的协程都阻塞了

func main() {

	chan1 := make(chan int)

	<-chan1
}

上面的例子就会引发 Deadlock,由于 channel 会阻塞,所以主程序一直阻塞在 <-chan1 等待数据,导致所有的协程都阻塞了,就会抛出 fatal error: all goroutines are asleep - deadlock! 的错误。

context

context 是 go 的一个标准库,目的是为了解决对于协程的控制。举个例子,如果想要从外部控制一个协程结束,只能使用 channel 传递变量来实现

go func(chan1 chan int, done chan bool) {
    for {
        select {
            case i := <-chan1:
            fmt.Println(i)
            //done channel 收到信号,确认退出
            case <-done:
            return
        }
    }
}(chan1, done)

time.Sleep(time.Second)

done <- true

在协程内部使用 select 监听 done channel,如果有收到数据就结束协程。这样,协程外部就可以通过向 done channel 传递信号来结束一个协程。

context 提供了一种新的方式从外部结束协程

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("go 1 end")
            return
        }
    }
}(ctx)

time.Sleep(time.Second)
cancel()   // 类似 done <- true
time.Sleep(time.Second)

可以看到,使用 context 的代码结构和 select + chan 的方式差不多,在 main 函数中创建了一个 context,然后把这个 context 当做参数传递给 goroutine,之后就可以通过这个 context 来跟踪 goroutine。

在协程中,使用 <- ctx.Done() 来判断是否收到结束协程的信号,在协程外部使用 cancel() 来发送信号结束协程。

从上面的逻辑可以看到使用 context 貌似和使用 chan + select 方式没有多大的不同。其实 context 的优势在以下几点:

context 控制多个协程

注意这里的控制多个协程有两种情况:

在上面的情况下,如果使用 chan 的控制方式,那么就需要创建多个 chan 来控制。但是如果使用 context 只需要一个 context 就可以控制。

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {

    go func(ctx context.Context) {
        for {
            select {
                case <-ctx.Done():
                fmt.Println("go 2 end")
                return
            }
        }
    }(ctx)

    for {
        select {
            case <-ctx.Done():
            fmt.Println("go 1 end")
            return
        }
    }
    
}(ctx)

time.Sleep(time.Second)
cancel()
time.Sleep(time.Second)

上面的代码演示了第 2 种情况下使用一个 context 控制多个协程结束。使用 context 就不用管理多个 chan 来控制多个协程。

使用 context 向协程传值

通过 context 可以传递额外的数据给协程

ctx, cancel := context.WithCancel(context.Background())

valCtx := context.WithValue(ctx, "key", "value")

go func(ctx context.Context) {
fmt.Println(ctx.Value("key"))

for {
    select {
        case <-ctx.Done():
        return
    }
}
}(valCtx)

time.Sleep(time.Second)
cancel()
time.Sleep(time.Second)

注意,虽然可以通过 context 向协程传递数据,但是一般只传递必要数据。

并发控制与 WaitGroup

在上面说明 Context 库的使用中,为了避免协程在结束之前 main 函数就结束掉,使用了 Sleep 函数来延迟 main 函数结束。其实除了使用 Sleep 函数这种粗糙的方式来控制协程之外,还可以使用 WaitGroup。

var wg sync.WaitGroup

wg.Add(2)
go func() {
    wg.Done()
}()

go func() {
    time.Sleep(time.Second)
    wg.Done()
}()

wg.Wait()

上面的实例代码展示了如何使用 waitGroup 来进行并发控制,waitGroup 有三个核心函数:

*****
Written by JayChen on 17 November 2018