Go 并发
本文将介绍 Go 对并发的支持。
一、进程与线程
具体请看:
二、CSP
传统编程语言 并非面向并发而生,它们对并发的支持往往依赖操作系统,利用的大多是操作系统提供的线程、进程间通信的原语(共享内存、信号、管道、消息队列、套接字等),并且使用更广泛的是结合了 线程同步原语 (锁、原子操作)的 共享内存方式 。这种基于共享内存的并发模型 难用且易错 ,程序员需要精心设计线程间的同步机制,考虑多线程间复杂的内存管理,考虑死锁的预防等。
Go 在并发模型的设计中借鉴了 Tony Hoare 提出的 CSP 并发模型 。CSP,Communicating Sequential Processes,通信顺序进程并发模型。CSP 旨在简化并发程序的编写,它认为输入输出应该是基本的编程原语,Process 只需要调用输入原语获取数据,顺序地处理数据,将结果数据通过输出原语输出即可。因此,一个符合 CSP 模型的并发程序应该是 一组通过输入原语、输出原语连接起来的 Process 的集合 。
Go 始终推荐以 CSP 并发模型风格构建并发程序,并且也提供了对传统的基于共享内存的并发模型的支持。
Don’t communicate by sharing memory, share memory by communicating. – Rob Pike
不要通过共享内存来通信,应该通过通信来共享内存。 – Go语言之父 Rob Pike
三、goroutine - 轻量级线程
1. 什么是轻量级线程?
Go 并没有将操作系统线程作为基本执行单元,而是自己实现了 goroutine,它是由 Go 运行时负责调度的轻量级线程。
相比操作系统线程来说,goroutine 具有以下特点:
资源占用小
- 操作系统线程栈大小固定,通常为 2M;goroutine 栈大小可变,通常初始大小为 2k
管理开销小,创建、销毁不需要系统调用
调度开销小,由 Go 运行时调度而不是操作系统调度,上下文切换只需要在用户模式下即可完成
2. 基本用法
通过 Go 函数 / 方法
创建 goroutine。
1 |
|
3. goroutine 是不是协程?
A goroutine is a lightweight thread managed by the Go runtime.
goroutine 并不是协程,其本质上是轻量级线程。
协程最关键的点在于:
协作式调度而非抢占式调度
- 协作式调度:各个任务之间相互合作,程序主动在需要等待时让出控制权
- 抢占式调度:各个任务之间不协作,互相争抢,外部调度器会中断运行并进行调度
四、channel - 通道
1. 什么是 channel?
channel 既可以用来实现 goroutine 之间的通信,也可以用来实现 goroutine 之间的同步。
2. channel 是一等公民
在 Go 中,channel 是一等公民,可以在代码中像使用普通变量一样使用它,例如:定义、赋值、传参、作为返回值、将 channel 放到其它 channel 中。
3. 创建 channel
1 |
|
创建一个元素类型为 int 的 channel,它的缓存区大小为 0。channel 的读写都会阻塞。
1 |
|
创建一个元素类型为 int 的 channel,它的缓存区大小为 num。缓存区未满时,写不需要阻塞;缓存区非空时,读不需要阻塞;否则,阻塞。
4. channel 的读写
1 |
|
5. 只读 / 只写的 channel
通过 <-
可以声明只读 / 只写的 channel,如下:
1 |
|
尝试从只写 channel 读值和向只读 channel 写值,都会导致编译错误。
通常而言,只读 / 只写的 channel 会被作为函数的形参,用以限制函数内部对 channel 的操作,例如:
1 |
|
6. 关闭 channel
可以通过 close()
函数关闭 channel。
channel 关闭后,所有从 channel 读取数据的操作都将返回,具体如下:
1 |
|
7. select
Go 提供了 select
关键字,用于同时在多个 channel 上进行 读值 / 写值操作。
1 |
|
需要注意的是:
- 每个
case
都应该是通道操作 select
会监听所有case
,- 如果有通道就绪,从这些通道中随机选择一个通道执行
- 否则,
- 如果有
default
,执行其中的与否 - 否则,阻塞
- 如果有