并发编程
翻译整理自 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)
}
}接收者永远不要
closechannel;向已关闭的 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 服务。