携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
作为一个对并发特性引以为傲的语言,Golang 对于实现线程间通信提供了 channel 的解决方案。想必大家都听过 Rob Pike 的名言:
Don't communicate by sharing memory, share memory by communicating.
今天我们就从这句话讲起,聊聊 Mutex 和 Channel 之间的那些事。
线程间通信
并发处理能够支持多个任务相互独立地执行,在 Golang 中,我们主要使用 goroutine(协程)来做到这一点。多个 goroutine 之间通过数据进行协作,就是 Rob Rike 所指的 communication(通信)。
一种实现通信的策略是使用各个线程都可以访问的公共变量。这样的做法存在隐患:若多个线程同时尝试修改公共变量,就会出现 race condition,我们无法判断到底哪个会成功,甚至程序会不会挂掉,这里的行为是 undefined 的,所以这里必须加锁。
既然要加锁,就不得不接受随着而来的性能消耗和复杂度。
在 Golang 中,由于我们在 goroutine 之外,还有 channel 来配合使用,这里就是语言层面的大杀器了。channel 是一个类型安全的 FIFO 队列,通过内部一些加锁机制(底层还是依赖 Mutex)保证并发安全。它可以说就是专门为了 goroutine 间通信创造的机制。
goroutine 可以选择将数据写入 channel,也可以作为一个 Reciver 监听 channel 中的消息。channel 的机制确保了每次 write 和 read 都是原子性的,作为使用者,我们不需要关心底层的调度机制,内存模型。
所以,Rob Pike 的这句名言想要传达的意思在于:Golang 提供了 channel 和 goroutine 来进行线程间信息传递,在合适的场景下,尽量使用这组搭配来解决问题会更优雅,而不用坚持使用【锁】+【公共变量】的方案来解决通信问题。
两个方案,一个是显式地将消息从一个线程传给另一个线程(goroutine+channel),另一个是多线程加锁共同访问同一个变量,从 Golang 语言支持的角度,我们都可以实现,那么到底哪个性能更好?分别适合什么场合呢?
channel vs mutex
从性能上看,channel 的实现是基于 mutex 的,同时也包含额外的上下文切换。做成任何事情都得等到被调度,处理,回复才能继续。这个再怎么优化,再怎么尊重cache locality,相比无竞争时的锁也没什么卵用。而好的并发代码的无竞争比例都挺高的,所以这是channel不得不面对的劣势。单纯从性能的层面,channel 是不如一把简单的 mutex 的。
channel的核心是数据流动,关注到并发问题中的数据流动,把流动的数据放到channel中,就能使用channel解决这个并发问题。那么到底有哪些数据流动的场景呢?
- 传递数据的所有权,即把某个数据发送给其他协程
- 分发任务,每个任务都是一个数据
- 交流异步结果,结果是一个数据
而 mutex 某段时间只给一个协程访问数据的权限。擅长数据固定的场景(这时仅仅解决掉并发访问即可),如缓存,状态。当只需要锁定少量共享资源时,使用 mutex 非常有用。
所以,当我们遇到通信问题时,先要去分析场景,数据的流动是否能通过 channel 在生产者和消费者两端进行分发,设想一下你的模型用 channel 和 mutex 写出来的复杂程度。选择最简单的即可。
总结
- 锁的本质是界定临界区,而channel的本质是通讯(Message Passing);虽然也可以用锁来实现通讯,用channel来界定临界区;
- channel 本质就是一个lock-based FIFO Queue 它天然地保证了上锁和解锁的顺序,所以使用起来很方便,也使它天然地易于解决race condition;
- 一个很小的上下文中,channel + goroutine的开销往往比 Mutex 要大,channel要和goroutine一起使用才有威力,而一个goroutine至少需要4k+的内存,而 mutex 的开销要小得多。当并行的逻辑和上下文变得复杂,那么这种开销就变得值得了,没必要去节省那一点点的内存的cpu时间;
- 如果任务处理模型中存在数据流动,数据的控制权需要在多个gorutine 中传递, 使用 channel 处理;
- 不存在数据流动,只需要锁定少量共享资源时,使用 mutex 处理。
A common Go newbie mistake is to over-use channels and goroutines just because it's possible, and/or because it's fun. Don't be afraid to use a
sync.Mutexif that fits your problem best. Go is pragmatic in letting you use the tools that solve your problem best and not forcing you into one style of code.
最后我想以官方 Mutex or Channel 博客中的这段话结尾。作为程序员,切忌教条主义,过度使用 channel+goroutine 的组合并不一定是对的。如果场景是静态的数据,没有流动性,直接走 Mutex 即可,务实最重要。