Go协程与通道机制
Don’t communicate by sharing memory; share memory by communicating.
Goroutine
goroutine 与 thread
在 Go 中,应用程序并发处理的部分被称作 goroutines
,它可以进行更有效的并发运算。协程由 Go 运行时 (runtime) 创建,和操作系统线程之间并无一对一的关系(user-kernel thread 映射模型):协程是根据一个或多个线程的可用性,映射(多路复用)在他们之上的。
协程调度器在用户态上对 goroutines 进行管理,采用 M:N 调度模型(M 个 Goroutine 映射到 N 个线程),可以用GOMAXPROCS
(逻辑处理器数量)来控制 N 的数量。相比之下,线程是由操作系统内核进行管理和调度的,被操作系统调度器分配到不同的处理器核心上运行。由于没有内核的介入,协程的创建与切换的开销降低很多。具体来说,Go 采用协作式调度(通过 Gosched()
或 channel
阻塞主动让出 CPU ),而线程则是由 OS 进行抢占式调度(RR, FCFS, …)。
摘选知乎的一个例子,生动解释了 Goroutine、coroutine 和 thread 之间的联系:
Thread:想象线程就像是公司的员工。每个员工都有自己的任务和责任,但他们共享公司的资源(例如办公室、打印机等)。员工(线程)的上下班(开始和结束线程)以及工作调度(线程切换)由公司管理层(操作系统)控制。如果公司要新增一个员工或者安排员工之间的工作,这需要管理层的直接参与,也会涉及到较多的人力和物力资源(也就是说,线程的创建和上下文切换成本相对较高)。
Coroutine:现在想象协程就像是在家工作的自由职业者。他们使用自己的电脑和办公设备(拥有自己的堆栈和局部变量),并且自己决定什么时候工作、什么时候休息(编程者控制)。他们可以随时暂停工作去喝杯咖啡或是散步(yield 或等待),然后再回来继续工作。所有这些活动的安排都不需要外部管理层的参与(用户级的调度),并且几乎不需要额外的资源(低成本的任务切换)。
Goroutine:Goroutine 就像是使用特殊工作方法的自由职业者团队。他们不仅可以自己安排工作时间(用户态调度),还使用一种特殊的通信方式 —— 他们不会直接交谈(共享内存),而是通过写信(传递消息)来沟通(channel 机制)。这种工作方式使他们的合作更加高效和有序(并发编程更容易实现和管理)。
GoLang GPM 模型 - Go 语言中文网 - Golang 中文社区介绍了 Go runtime GPM 调度模型,其中很清晰的解释了线程模型 👍
select
在 Go 里,select
语句主要用于在多个通道操作间进行选择。它和switch
语句类似,不过switch
用于选择不同的条件分支,而select
用于选择不同的通道操作。select
会阻塞,直到其中一个通道操作准备好,若有多个操作同时就绪,会随机选择一个执行。要是存在default
分支,在没有通道操作准备好时,会执行default
分支,从而避免阻塞。
1 | // 示例:实现超时控制 |
select
机制是 Go 语言并发编程中的一个重要特性,它为处理多个通道的并发操作提供了一种简洁、高效且灵活的方式,使得 Go 语言在处理复杂的并发场景时能够更加优雅和易于维护。
- 实现并发操作的多路复用:在并发编程中,经常需要同时处理多个通道的操作。
select
允许程序在多个通道之间进行选择,当其中任何一个通道准备好进行读取或写入操作时,就可以执行相应的分支逻辑。这使得程序能够高效地处理多个并发任务,避免了逐个检查通道状态的繁琐操作,提高了代码的简洁性和可读性。 - 处理异步事件:在异步编程模型中,各个操作可能在不同的时间点完成,
select
可以用于监听多个异步操作的完成信号。例如,在网络编程中,可能同时有多个网络连接在进行数据传输,通过select
可以随时响应哪个连接有数据可读或可写,从而实现对多个网络连接的高效管理。
Channel
无缓冲同步与有缓冲异步的数据传递
一般我们通过ch := make(chan type)
创建的是无缓冲且同步的通道,只有当接收方准备好后发送方才会发送数据,因此通道的发送 / 接收操作在对方准备好之前是阻塞的。
1 | // 阻塞示例 |
一个无缓冲通道只能包含 1 个元素,通过ch := make(chan type, buf)
可以设置通道缓存大小。在缓冲区满载之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲区空了。
如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收,此时上面的示例可以运行。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。
底层数据传递原理
- 数据复制:当一个
goroutine
向通道发送数据时,数据会被复制到通道内部。在无缓冲通道中,这个复制操作是在接收者准备好接收数据时才会发生。 - 同步机制:通道使用了互斥锁(mutex)和条件变量(condition variable)来实现同步。当发送者尝试发送数据时,它会先获取通道的锁
mutex_lock()
,检查是否有接收者准备好接收数据。如果没有,发送者会释放锁mutex_unlock()
并进入阻塞状态;当接收者准备好接收数据时,它会获取锁,通知发送者可以发送数据signal()
,然后进行数据的复制和接收操作。
关闭通道与测试阻塞
通道可以被显式的关闭close(chan)
,只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道,因此只有发送者才会有关闭通道的需要。给已经关闭的通道发送或者再次关闭都会导致运行时 panic。
1 | func send() chan int { |
相应的,接收方需要一种方法检测通道有没有被阻塞(或被关闭)。
1 | func receive(ch chan int) { |
参考资料:
[1] https://learnku.com/docs/the-way-to-go/141-concurrency-parallel-and-co-process/3685
[2] Goroutine 和线程比较 - 知乎
[3] GoLang GPM 模型 - Go 语言中文网 - Golang 中文社区
[4] https://fafucoder.github.io/2021/11/08/golang-goroutine/