在开始之前,需要了解并发(concurrency)和并行(parallesim)的区别。
- 并发:逻辑上具备处理多个同时性任务的能力。
- 并行:物理上同一时刻执行多个并发任务。
我们通常会说程序是并发设计的,也就是说它允许多个任务同时执行,但实际上并不一定 真在同一时刻发生。 在单核处理器上,它们能以间隔方式切换执行。而并行则依赖多核处 理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。 简单点 说,并行是并发设计的理想执行模式。
多线程或多进程是并行的基本条件,但单线程也可用协程(coroutine)做到并发。 尽管协 程在单个线程上通过主动切换来实现多任务并发执行,但它也有自己的优势。 除将因阻塞 而浪费的时间找回来外,还免除了线程切换开销,有着不错的执行效率。协程上运行的多 个任务本质上是依旧串行的,加上可控自主调度, 所以并不需要做同步处理。
即便采用多线程也未必就能并行。Python 就因 GIL 限制,默认只能并发而不能并行。大多时候 采用 “多进程 + 协程” 架构模型。
很难说哪种方式更好一些,它们有各自适用的场景。通常情况下,用多进程来实现分布式 和负载平衡,减轻单进程垃圾回收压力;用多线程(LWP)抢夺更多的 处理器资源;用协 程来提高处理器时间片利用率。 简单将 goroutine 归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务 单元可被调度到其他线程并行执行。这更像是多线程和协程的 综合体,能最大限度提升执 行效率,发挥多核处理能力。 只需在函数调用前添加 go 关键字即可创建并发任务。
go println("hello, world!")
go func(s string) {
println(s)
}("hello, world!")
注意是函数调用,所以必须提供相应的参数。 关键字 go 并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列 中,等待调度器安排合适系统线程去获取执行。当前流程不会阻塞,不会等待该任务启动, 且运行时也不保证并发任务执行次序。 每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统 默认 MB 级别的线程栈,goroutine 自定义栈初始仅需 2 KB,所以才能创建成千上万的并 发任务。自定义栈采取按需分配策略, 在需要时进行扩容,最大能到 GB 规模。
与 defer 一样,goroutine 也会因 “延迟执行” 而立即计算并复制执行参数。
var c int
func counter() int {
c++
return c
}
func main() {
a := 100
go func(x, y int) {
time.Sleep(time.Second) //让goroutine 在main逻辑之后执行
println("go:", x, y)
}(a, counter()) // 立即计算并复制参数
a += 100
println("main:", a, counter())
time.Sleep(time.Second * 3) //等待 goroutine 结束
//
}
输出:
main: 200 2
go: 100 1