并发编程

翻译整理自 A Tour of Go - Concurrency、Go by Example - Goroutines 与 Effective Go - Concurrency。

Go 通过 Goroutine 和 Channel 提供轻量级并发支持。其设计哲学来自 Hoare 的 CSP(Communicating Sequential Processes):不要通过共享内存通信,而应通过通信共享内存。

1. Goroutine

go 关键字启动一个新的并发执行单元(goroutine):

func say(s string) {
    for i := 0; i < 3; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("hello") // 异步执行
    say("world")   // 主 goroutine
}

goroutine 由 Go 运行时调度,栈初始只有 2KB,单机可启动百万级 goroutine。

注意:主函数返回时不会等待 goroutine,需要用 sync.WaitGroup 或 channel 同步。

sync.WaitGroup

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        fmt.Println(i)
    }(i)
}
wg.Wait()

2. Channel

ch := make(chan int) // 无缓冲 channel

go func() {
    ch <- 42       // 发送
}()

v := <-ch          // 接收

无缓冲 channel

发送会阻塞直到有人接收,相当于同步握手:

ch := make(chan int)
go func() {
    fmt.Println("greeting from goroutine")
    ch <- 1
}()
<-ch // 阻塞等待

有缓冲 channel

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // 阻塞,缓冲已满

3. range 与 close

发送方可以 close channel,接收方通过 range 遍历直到关闭:

func Fibonacci(n int, ch chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        ch <- x
        x, y = y, x+y
    }
    close(ch) // 必须 sender 调用 close
}

func main() {
    c := make(chan int, 10)
    go Fibonacci(cap(c), c)
    for v := range c {
        fmt.Println(v)
    }
}

接收者永远不要 close channel;向已关闭的 channel 发送会 panic。

4. select 多路复用

select 同时等待多个 channel,就绪时随机选择一个:

tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)

for {
    select {
    case <-tick:
        fmt.Println("tick.")
    case <-boom:
        fmt.Println("BOOM!")
        return
    default:
        // 无就绪时执行(避免阻塞)
    }
}

5. sync.Mutex 互斥锁

某些场景必须用共享内存:

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

RWMutex

读多写少用 sync.RWMutex:

var rw sync.RWMutex
rw.RLock()   // 读锁
rw.RUnlock()
rw.Lock()    // 写锁
rw.Unlock()

6. 并发模式

Worker Pool

jobs    := make(chan int, 100)
results := make(chan int, 100)

for w := 1; w <= 3; w++ {
    go worker(w, jobs, results)
}
for j := 1; j <= 5; j++ {
    jobs <- j
}
close(jobs)
for r := 1; r <= 5; r++ {
    <-results
}

Fan-out / Fan-in

将任务拆分到多个 goroutine,再通过 channel 汇总结果。

7. context.Context

长链路请求需要超时与取消,使用 context:

ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()

select {
case <-ctx.Done():
    fmt.Println("timeout:", ctx.Err())
case res := <-doWork(ctx):
    fmt.Println(res)
}

小结

不要通过共享内存通信,而应通过通信共享内存。

掌握 goroutine、channel、select、sync、context 后,你已经能写出高并发、易维护的 Go 服务。