1.介绍

CSP模型看上去类似于actor模型,但是区别在于:actor模型的重点在于参与交流的实体,而CSP模型的重点在于用于交流的通道。

2. CSP和Actor

简单来说actor模型的第一类对象是并发执行的实体actor而CSP模型的第一类对象是传递消息的通道。

使用actor模型的程序是由独立的、并发执行的实体(actor,Elixir中称为进程),这些实体之间通过发送消息进行通信。每个actor都有一个信箱,用于保存已经收到但尚未被处理的消息。

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

下面通过core.async这个clojure的库说明这种并发模型。

3. core.async库

3.1 channel

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

缓冲区满的时候有以下策略:

  1. 阻塞型(blocking):默认情况下,channel是同步的(或称无缓存的)——一个任务向channel写入消息的操作会一直阻塞,直到另一个任务从channel中读出消息
  2. 弃用新值型(dropping):后面来的弃掉,这样不会阻塞
  3. 移出旧值型(sliding):原来旧的值弃掉,也不会阻塞

之所以不涉及自动增大的缓存区,是因为资源总会枯竭,未来总归会有更大规模的问题。不设计成自动增大的缓存区,迫使是思考相应的策略,避免未来某个时间出现一个破坏性极强、隐蔽极深切难以诊断的bug。

3.2 go块

go块表示用go关键字包起来的代码块。

线程池技术时处理CPU密集型任务的利器。较多阻塞线程的时候会造成线程无限期被占用,影响了线程池的使用。有一些解决方案,比如事件驱动的编程风格。不过这些方案破坏了控制流的自然的表达形式,让代码变得难以阅读和理解。更糟糕的是,这些方案还会大量使用全局状态,因为事件处理需要保存一些数据,以便之后的事件处理器使用。我们已经学习过这个结论:状态和并发最好不要混用

go提供了一种两全其美的解决方案——既可以写出事件驱动的代码来解决目前碰到的阻塞问题,又可以不牺牲代码的结构性和可读性。原因是**go块在底层将串行化代码透明地重写成了事件驱动的形式。

控制反转:go块中的代码会被转化成一个状态机。当从channel中读出消息或者向channel中写入消息时,状态机将暂停,并释放它锁占用的线程的控制权。当代码可以继续运行时,状态机进行一次状态转换,并可能在另一个线程中继续运行。通过这样的控制饭庄,可以在有限的线程池中高效地运行多个go块。

补充:这种go块状态机执行的实现,应该就是大家常说的协程了。

4. 总结

4.1 优点

与actor模型相比,CSP模型的最大的优点是灵活性。使用actor模型时,负责通信的媒介与执行单元是紧耦合的——每个actor都有一个信箱。而channel在CSP模型中作为第一类对象,可以被独立创建和读写数据也可以在不同的执行单元传递,更加灵活。CSP的异步编码上逻辑更加清楚,符合人脑思维模式。

4.2 缺点

CSP模型没有OTP这种强大的支持分布式和容错性的库,同时也存在死锁问题和没有提供良好的并行支持。