并发编程(九) - 总结

44 阅读2分钟

在并发编程系列中,我们详细介绍了 atomic、mutex、sem、waitgroup、once、sync.map、pool 等并发原语。这些原语使用起来很简单,弄清楚原理似乎并不那么容易。我们不仅要会用,还要学会如何选择合适的并发原语。

并发编程产生的本质是多核 cpu 架构下,相互独立运行的 cpu 代码在操作同一块内存时,未控制正确的访问顺序而导致的数据异常。为了解决这一问题,而诞生了并发编程这一课题。

为了保证多个 cpu 访问同一内存地址的顺序,则需要 cpu 本身提供原子操作的能力。即最底层就要提供一些指令,在同一时刻,只能有一个 cpu 执行。基于原子命令,可以实现信号量机制,从而保护临界区。在 go 中,对底层的命令进行封装,屏蔽 cpu 的指令细节,暴露统一的原子操作 api。互斥锁则又是更高一层的抽象,通过互斥锁可以直接保护对应的临界区。除了控制访问资源的数量外,并发编程的另一问题就是通信了。相较于其他的编程语言,go 设计了一种新的类型 channel。可以把它看作是一个有锁队列,用于连接多个 goroutine 进行通信。此外,也封装了 waitgroup,singlefligh 等组件,用于控制 goroutine 的调度。

从功能上来看,无论是使用 atomic、mutex、channel 似乎都能实现相同的效果,那我们具体要怎么选型呢?首先要考虑的问题就是开发成本,对于一个问题,如果现成的库能够直接解决,则直接使用。如果现成的库无法解决,则优先考虑使用 channel, 它使我们更容易理解代码。对于一些对性能要求特别高的场景,则需要考虑直接使用 atomic 或者像 pool 一样利用 GMP 调度模型减少锁操作,并开发实现相关的功能。