5

CSP 的用例

CSP 的用法最早是 Go 语言传开来的, 看一下我从网上扒的代码:

package main

import "fmt"

func ping(pings chan<- string, msg string) {
    pings <- msg
}

func pong(pings <-chan string, pongs chan<- string) {
    msg := <-pings
    pongs <- msg
}

func main() {
    pings := make(chan string, 1)
    pongs := make(chan string, 1)
    ping(pings, "passed message")
    pong(pings, pongs)
    fmt.Println(<-pongs)
}

其中 <- 符号是往 channel 当中写入数据的操作.
同时注意一般 <- 的位置对于 goroutine 来说是阻塞的,
由于 channel 能够处理异步操作, 也就是说能做到异步代码用同步写法.
更多的细节搜索 "go channel" 应该就能找到.

除了 Go, Clojure 也实现了对于 CSP 的支持, 也就是 core.async 这个库,
在 Clojure 当中语法做了调整, 成了 >! <! 这样的写法, 有点怪,
但是基本功能差不多, >!<! 都是模仿的阻塞, channel 概念也一样:

(let [c1 (chan)
      c2 (chan)]
  (go (while true
        (let [[v ch] (alts! [c1 c2])]
          (println "Read" v "from" ch))))
  (go (>! c1 "hi"))
  (go (>! c2 "there")))

这个例子当中 (chan) 生成 channel, 然后用 go 生成 3 个线索...
虽然用了 while true, 但是通过 alts! 也形成了阻塞.
更新细节搜索 "core.async" 可以找到.

为什么用 CSP

看 Wiki https://en.wikipedia.org/wiki...

In computer science, communicating sequential processes (CSP) is a formal language for describing patterns of interaction in concurrent systems.[1] It is a member of the family of mathematical theories of concurrency known as process algebras, or process calculi, based on message passing via channels. CSP was highly influential in the design of the occam programming language,1 and also influenced the design of programming languages such as Limbo[3] and Go.[4]

CSP 本来是用于描述并发的系统之间如何交互的, 也就是在 Go 当中的用法.
由于并发的操作通常都是异步的, 所以 CSP 也能适合异步的行为.
最主要的概念就是 Channel, 也叫做"管道", Channel 可以用于传输数据,
因而就有对于管道读和写的操作, 分别是 take!put!, Clojure 里的叫法.
前面说了, 管道看上去是阻塞代码执行的, 也就是说读和写可以进行等待.
这样就能模拟一些场景, 比如抓取网络数据再打印, 就很容易写出来了.

常见功能还有 alts!, 对应 Go 的 select, 就是多个 Channel 取首先返回的数据,
还有 merge 记不大清, 好像是汇总多个 Channel 返回的数据, 变成一个?
其他的 filter, map 等等序列的操作, 在 Channel 上也有类似实行,
另一方面 CSP 在实用当中应该是进行了扩展, 实际功能不止这些.
比如说增加了 (timeout 1000) 这样的 Channel 等待一秒返回数据,
还有对 Channel 进行 Buffer 的功能, 以及异步的推数据等等.

听起来很花哨, 但是如果有动画可以展示一下就很清楚了, 我还没找到...
从整体的思路上说, 这是对于异步事件的一种抽象, 可以用来实现很多业务.
想想办法再解释细节吧, 我这里先介绍 JavaScript 这边的情况...

js-csp 的现状

由于 Node 6 开始有 yield, 用同步代码写异步成为了可能,
于是有就有了 js-csp 这个模块, 通过 yield 实现了 CSP 的功能,
我还看到一个用了 async 的, 估计不能用, 但是供参考:

https://github.com/ubolonton/...
https://github.com/dvlsg/asyn...

我直接贴一遍 README 当中的例子, 自己看看能不能看懂:

function* player(name, table) {
  while (true) {
    var ball = yield csp.take(table); // 等待取得数据
    if (ball === csp.CLOSED) { // 关闭状态特殊处理
      console.log(name + ": table's gone");
      return;
    }
    ball.hits += 1;
    console.log(name + " " + ball.hits);
    yield csp.timeout(100); // 等待延时结束
    yield csp.put(table, ball); // 推数据并等待对方取
  }
}

csp.go(function* () {
  var table = csp.chan(); // 创建 Channel

  csp.go(player, ["ping", table]); // 相当于启动 goroutine
  csp.go(player, ["pong", table]); // 相当于启动 goroutine

  yield csp.put(table, {hits: 0}); // 推数据等待对方取
  yield csp.timeout(1000); // 等待延时结束
  table.close();
});

运行的效果是:

=>> node go.js
ping 1
pong 2
ping 3
pong 4
ping 5
pong 6
ping 7
pong 8
ping 9
pong 10
ping: table's gone
pong: table's gone

这样模拟的就是两个进程之间相互发送数据的场景.

但实际上 CSP 可以对事件流进行抽象, 也就能做出更强大的功能.
这就是在我之前推荐的这篇文章上的做的介绍, 点进去看吧:
http://jlongster.com/Taming-t...

随着浏览器和 Node 对 yield 支持的完善, 使用 js-csp 已经可以做到.
考虑到方案的灵活性, 我认为值得往深了去挖一挖.

和 Rx 的对比

事件流的另一套有名的方案就是 Rx, 有 js 版本的 Rxjs.
大概来说, Rx 是用 OOP 语法封装的 FP 风格的响应式编程方案, 操作繁多,
而 CSP 通过管道提供的是一些灵活但过于基础的原语,
看社区的讨论, 其实有很大的重叠的部分, 尽管细节还很难说...
我搜集了一些文章:

https://medium.com/@puppybits...

还有 GitHub 上的一些讨论:

https://github.com/ubolonton/...
https://github.com/cyclejs/cy...

另外还有某人用 Rx 写法模仿 CSP 方案的博客:

http://swannodette.github.io/...
http://swannodette.github.io/...
http://potetm.github.io/2014/...
http://potetm.github.io/2014/...

小结

说起来我还没怎么消化这东西.. 但是如果看过文章里的 Demo, 你一定印象深刻,
流是数据和时间绑定在一起形成的产物, 普通的编程手法很难处理,
但是 CSP 的概念让处理 Channel 中传递的数据成为了而比较灵活的事情.
参考国外社区的评论, 这是具备相当大价值的一块知识, 所以在持续跟进.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者