七周七并发模式:通信顺序进程(CSP)

Posted by 蔡华的博客 on April 2, 2018

概念

CSP

与actor模型类似,通信顺序进程(Communicating Sequential Processe,CSP)模型也是由独立的、并发执行的实体所组成,实体之间也是通过发送消息进行通信。但两种模型的重要差别是:CSP模型不关注发送消息的实体,而是关注发送消息时使用的channel(通道)。channel是第一类对象,它不像进程那样与信箱是紧耦合的,而是可以单独创建和读写,并在进程之间传递。

  • 从这段话可以看出CSP本质上也是独立运行的执行单元,但是它没有mailbox,那么它执行的数据来自哪里呢?来自于channel。

Channel

一个channel就是一个线程安全的队列,任何任务只要持有channel的引用,就可以向一端添加消息,也可以从另一端删除消息。在actor模型中,消息是从指定的一个actor发往指定的另一个actor的;与之不同,使用channel发送消息时发送者并不知道谁是接收者,反之亦然。

  • 也就是说CSP更像是在不同的actor之间共享mailbox,但是这里mailbox编程了channel。

默认情况下,channel是同步的(或称无缓存的)——一个任务向channel写入消息的操作会一直阻塞,直到另一个任务从channel中读出消息。

  • emmm,阻塞了一个写入的线程。这样有些浪费线程,因此有了带缓存区的channel。当channel的缓存区有足够空间时,向其中写入消息的操作会立刻完成,不会阻塞。
  • 对于有缓存的channel,一次性写入超过缓存区大小的数据时策略也是不同的,可以是只保留前面写入的(dropping),也可以是保留后面写入的(slide),甚至是直接阻塞式的(blocking),这个一般都有语言级别的策略支持。
  • PS:是否支持可扩展的buffer这个问题书中提到是不支持,不过可能也只是这个语言不支持。因为我还没有看过其它的编程语言中是如何处理buffer的。
  • channel也可以被关闭,此时read/write的处理不同的语言可能会有不同的处理方式。

go块

  • 在说这个概念之前需要先看看在它之前的线程操作过程中遇到的问题,只有弄清楚了存在什么问题,我们才能理解为何会有go块,而它又是干什么的。

线程启动和运行时都有一定开销,这正是现在的程序都避免直接创建线程、转而使用线程池的原因。然而线程池并不总是适用。尤其是当程序阻塞时,使用线程池可能会造成麻烦。

线程池技术是处理CPU密集型任务的利器——任务进行时会占用某个线程,任务结束后将线程返还给线程池,使线程可以被复用。但涉及线程通信时使用线程池是否仍然合适呢?如果线程被阻塞,那么它将无限期被占用,这就削弱了使用线程池技术的优势。

这种问题是有一些解决方案的,但它们通常会对代码风格加以限制,使之变成事件驱动的形式。虽然这些方案都能解决问题,但它们破坏了控制流的自然的表达形式,让代码变得难以阅读和理解。更糟糕的是,这些方案还会大量使用全局状态,因为事件处理器需要保存一些数据,以便之后的事件处理器使用。

  • 从上面的摘抄中已经可以看出来多线程、线程池其实是各种局限,而加入了事件驱动后也是有问题的。在这样的情况下,一个能够支持事件驱动,又可以保证代码在编写和阅读时看上去是顺序执行(这样比较符合人的阅读习惯和理解),同时还把状态数据封装起来的东西,emmm,此时我第一个反应就是unity中的Coroutine,而unity的coroutine本质是个状态机。这样一路的推导下来,我们也就明白了go块的本质就是一个状态机。而它的诞生也是为了解决上面说提到的种种问题。
  • 本书中对于go块的使用demo使用了Clojure这个语言,我觉得没必要细说具体实现,因为每个语言也不同。不过go块中需要强调一点,它是非阻塞的。当然如果是阻塞的并不是不能工作,但是又回到了线程大量使用而又阻塞的问题了。
  • 在书中提到go块的成本很低,这与线程不同,因为目前我所知道的语言中go块这样的概念都是基于协程的。golang中甚至没有线程的概念。而C#中task也是协程的。

使用CSP

  • 实际上本章第二节的例子基本都是利用channel的同步性。比如超时处理就可以让一个定时器过一定时间后写入channel,在这之前另一个方法用来读取channel。
  • 还有一些异步操作也是利用了channel的同步性,在以前可能异步操作需要使用回调,但是这样的话不同函数就成了互相调用的情况,但是使用CSP可以让函数基于channel来执行。比如:A函数读取一个channel的数据,在channel被写入数据前,调用A的线程阻塞;在IO操作完成后将结果写入channel,这样A函数就可以读取IO的结果了,而且两个函数不需要知道对方的存在,完全依赖channel来通信。

总结

  • 首先与Actor比,CSP更加灵活。Actor负责通信的媒介(mailbox)与执行单元是紧耦合的,而CSP中channel可以被独立的创建、读写数据。从耦合性上将CSP更好。
  • 然后CSP的go块使得异步编程更加高效,比起写很长的回调式的代码,CSP的代码更简洁。
  • 至于书中提到的CSP的缺点:分布式和容错性支持的不够好,这个我个人觉得因语言而异,因为很少做相关的开发不好发表意见。

参考