goroutine
这是Go最大的特点。
必须掌握:
- goroutine
- channel
- select
- sync.WaitGroup
- Mutex
- RWMutex
- Once
- Cond
- atomic
按照下面顺序系统学习:
yaml
1. 并发基础概念
|
v
2. goroutine(轻量级线程)
|
v
3. channel(通信机制)
|
v
4. select(多路复用)
|
v
5. sync.WaitGroup(等待任务完成)
|
v
6. Mutex / RWMutex(共享内存同步)
|
v
7. Once(只执行一次)
|
v
8. Cond(条件变量)
|
v
9. atomic(无锁原子操作)
一、Go 并发设计思想
先理解一句 Go 的哲学:
不要通过共享内存来通信,而应该通过通信来共享内存。
英文:
vbnet
Don't communicate by sharing memory;
share memory by communicating.
这句话非常重要。
java传统模型
多个线程访问同一块内存:
count++;
需要:
arduino
synchronized
Lock
volatile
AtomicInteger
这叫:
数据竞争(Data Race)
Go 更推荐:
css
goroutine A
|
|
channel
|
|
goroutine B
数据通过 channel 流动。
二、goroutine
Java Thread
操作系统线程:
markdown
Java Thread
|
|
OS Thread
|
|
CPU Core
线程由 OS 调度。
创建成本:
makefile
线程栈:
1MB左右
10万个线程:
100GB内存
几乎不可行。
Go goroutine:这部分详细请看本专栏的GMP详解
结构:
markdown
goroutine
|
|
Go Runtime Scheduler
|
|
OS Thread
|
|
CPU
Go 自己实现调度。
叫:
GMP 调度模型
三、创建 goroutine
最简单:
go
package main
import (
"fmt"
"time"
)
func hello(){
fmt.Println("hello")
}
func main(){
go hello()
time.Sleep(time.Second)
}
关键:
scss
go hello()
表示:
创建一个新的 goroutine。
执行流程:
main goroutine:
yaml
main
|
|
创建
|
v
hello goroutine
两个并发执行。
四、goroutine 生命周期
非常重要。
例如:
go
func main(){
go func(){
fmt.Println("hello")
}()
}
运行:
css
main结束
↓
整个程序退出
↓
所有goroutine结束
所以:
输出:
可能:
什么都没有
因为:
goroutine还没有运行。
所以要有主次之分,生产环境中,我们常用WaitGroup,让主协程来管理所有的goroutine
五、goroutine 调度
Go不是:
一个goroutine对应一个线程
而是:
markdown
100万个goroutine
|
|
scheduler
|
|
几个线程执行
例如:
服务器:
10000请求
↓
10000 goroutine
↓
几十个线程
所以 Go 非常适合:
- 网络服务
- RPC
- 微服务
- IO密集任务
六、goroutine 常见问题
1. goroutine泄漏
例如:
go
func worker(ch chan int){
for{
data:=<-ch
fmt.Println(data)
}
}
如果:
没人发送:
worker永久阻塞
goroutine泄漏。
生产环境:
必须考虑:
退出机制
通常:
context。
2. 闭包变量捕获问题
经典:
错误:
css
for i:=0;i<10;i++{
go func(){
fmt.Println(i)
}()
}
输出:
可能:
erlang
10
10
10
...
原因:
所有goroutine共享:
css
同一个i地址
正确:
css
for i:=0;i<10;i++{
i:=i
go func(){
fmt.Println(i)
}()
}
或者:
css
for i:=0;i<10;i++{
go func(n int){
fmt.Println(n)
}(i)
}
7、面试重点
问题一:为什么 goroutine 比线程轻?
回答:
goroutine 由 Go runtime 管理,采用 GMP 的 M:N 调度模型,不需要一个 goroutine 对应一个 OS Thread;同时 goroutine 初始栈很小,而且栈可以动态扩缩容。因此相比传统 OS Thread,goroutine 的创建和调度成本更低,可以支持大量并发 goroutine。
问题二:goroutine 什么时候切换?
回答:
goroutine 在阻塞或者被调度器抢占时会发生调度切换。常见情况包括系统调用、channel 阻塞、网络 IO、定时器等待,以及 Go runtime 的抢占式调度。