GoLang 并发控制

2018-09-10 language golang

协程不像进程或者线程可以被抢占,也就是可以强制退出,但是协程需要通过协作方式完成调度,也就是只能主动退出,如下是几种常见的退出方式。

管道

for-range

通过 range 可以感知到管道的关闭,当管道关闭的时候,range 就会结束退出。

go func(in <-chan int) {
    for x := range in {
        fmt.Printf("Process %d\n", x)
    }
}(inChan)

并发控制

控制并发有两种经典的方式:WaitGroupContext

WaitGroup

一种控制并发的方式,它的这种方式是控制多个 goroutine 同时完成,有点类似于 waitpid() 函数,用于多个协程同步,通过 Add() 增加,Done() 减小,wait() 会等待到 0 。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("No. #1 Done")
        wg.Done()
    }()
    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("No. #2 Done")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("OK, All Done")
}

Channel

简单来说,就是通过 ChannelSelect 这种方式进行同步。

package main

import (
    "fmt"
    "time"
)

func main() {
    stop := make(chan bool)
    go func() {
        for {
            select {
            case <-stop:
                fmt.Println("Quit now")
                return
            default:
                fmt.Println("Running")
                time.Sleep(1 * time.Second)
            }
        }
    }()
    time.Sleep(5 * time.Second)
    fmt.Println("Time up...")
    stop <- true
    fmt.Println("Bye Bye")
}

如上的例子中,通过预先定义的一个 chan 来通知后台的协程,这种方式比较适合与一些可预期的简单场景,例如有多个协程需要控制、协程又派生了子协程。

Context

对于上述的场景,常见的是 HTTP 请求,每个 Request 都需要开启一个协程,同时这些协程又有可能派生其它的协程,例如处理身份认证、Token校验等;Context 就是提供了一种协程的跟踪方案。

对于 go1.6 及之前版本使用 golang.org/x/net/context 而在 1.7 版本之后已移到标准库 context 。

简介

Context 的调用应该是链式的,通过 WithCancelWithDeadlineWithTimeoutWithValue 派生出新的 Context,当父 Context 被取消时,其派生的所有 Context 都将取消。

上述 WithXXX 返回新的 ContextCancelFunc,调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器;未调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。

context.Background()/TODO() 会返回一个空 Context,一般将该 Context 作为整个 Context 树的根节点,然后调用 withXXX 函数创建可取消的子 Context,并作为参数传递给其它的协程,从而实现跟踪该协程的目的。

如下是一个示例。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx, "CTX1")
    go watch(ctx, "CTX2")
    go watch(ctx, "CTX3")
    time.Sleep(5 * time.Second)
    fmt.Println("Time up...")
    cancel()
    time.Sleep(1 * time.Second)
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "Quit now")
            return
        default:
            fmt.Println(name, "Running")
            time.Sleep(1 * time.Second)
        }
    }
}