1. 理论基础
CSP(Communicating Sequential Processes) 和Actor是两个当下流行的并发模型,其中CSP模型的代表是golang的channel,Actor的代表是erlang编程。不论是CSP还是Actor,他们都完完全全地贯彻了一句至理名言:
Don't communicate by sharing memory; share memory by communicating. (R. Pike)
与这两者并发模式相对比的是基于锁的并发编程,锁通过强制数据同步。其中Actor直接直接通讯(ActorA ====> ActorB),CSP之间通过Channel通讯(ChannelA==>GoChannel, GoChannel==>ChannelB),CSP更加松耦合。
2. ectd chan源码示例
在etcd源码中大量使用了channel进行数据交换,尤其是raft协议的实现部分。这里举一个打开文件的例子。其中在newFilePipeline 方法中创建filePipeline结构体,并通过go fp.run() 启动了一个goroutine。在run方法中打开了一个临时文件,并且通过select操作阻塞goroutine。
外部调用通过使用Open方法,获取到临时文件。当临时文件被获取时,filec chan中的数据被取出,此时run方法不再阻塞,它将创建一个新的临时文件。这里etcd的操作是使用临时目录完成初始化操作再将其重命名的方式, 这里主要是为了让整个初始化过程看上去是一个原子操作
3. channel与锁的对比
毫无疑问channel底层也是需要同步机制已保证数据操作的有序性的,它的作用就是为用户封装了一个简单易用的chan,从而不需要掌握太复杂的锁、原子操作等复杂操作,也能使用这种效率更高的并发编程方法。其中再golang文档的内存模型说明中有关于goroutine、chan、锁、once的使用示例,尤其是文档再后面给出的错误同步示例,对认识多线程的同步很有意义。
那么该什么时候锁,什么时候使用chan呢?在MutexOrChannel文档中有简单的说明,其中Channel和Mutex擅长处理的情况如下表。
| Channel | Mutex |
|---|---|
| 传递数据所有权 分配工作单元 传递异步调用结果 | 缓存 状态 |
文档中有一句话很重要:
If you ever find your sync.Mutex locking rules are getting too complex, ask yourself whether using channel(s) might be simpler.
在开发实践中,如果不能很清晰的区分出哪个方案更好,可以考虑Channel+Mutex的设计,另外WaitGroup也是一个很重要的原生同步方法。