发现一个月没刷技术文章了, 有点慌, 整理一篇短的 CSP 用法出来,
只包含最基本的用法, 在 Go 里边最清晰, 不过我是在 Clojure 写的 CSP.
js 版本的 CSP 实现包含异步, 用法会更繁琐一些, 但是也值得看看.
我相信 async/await 普及之前, js-csp 还是一个很有意思的选择.
我的代码写的是 CoffeeScript, 可以自动脑补圆括号花括号上去...
注意包含 yield
的函数自动被转成 function*() {}
, 所以注意脑补.
脑补不出来的只好贴在这边编译下了 http://coffeescript.org/
使用 timeout
首先是最基本的 CSP 的例子, 也就是用同步的代码写异步的逻辑,
CSP 当中最核心的概念是 Channel, 最简单的 csp.timeout(1000)
创建 channel.
csp = require 'js-csp'
# 用 csp.go 启动一个 yield 函数
csp.go ->
# 有 csp.take 从这个管道取出数据, yield 来模拟阻塞的效果
yield csp.take csp.timeout(1000)
console.log 'Gone 1s'
运行一下:
=>> coffee async.coffee
Gone 1s
我注意到对于 timeout
来说, 省掉 csp.take
也是能够正常运行的:
csp = require 'js-csp'
csp.go -> # 脑补 yield 函数
yield csp.timeout 1000
console.log 'Gone 1s'
yield csp.timeout 2000
console.log 'Gone 2s'
yield csp.timeout 3000
console.log 'Gone 3s. End'
运行一下:
=>> coffee async.coffee
Gone 1s
Gone 2s
Gone 3s. End
使用 put 和 take
csp.timeout
比较特殊, 默认就会产生数据, 只要进行 csp.take
就好了.
一般的 Channel 的话, 需要手动创建出来, 然后手动推数据,
比如下面的代码创建了一个数据, 用 csp.go
启动另一个"进程"往 Channel 推数据,
这里的"进程"的说法并不是真正的进程, 只是模拟进程的行为:
csp = require 'js-csp'
talk = (ch) ->
yield csp.timeout 3000
console.log 'Done 3s timeout'
# 等待 3s 然后往 Channel 当中写入数据, yield 会产生等待
yield csp.put ch, 'some result'
csp.go ->
ch = csp.chan()
# 启动另一个"进程"
csp.go talk, [ch] # 数组里是传给 talk 函数的参数
# 使用 yield.take 从 Channel 取出数据, 使用 yield 模拟等待
result = yield csp.take ch
console.log 'Result:', JSON.stringify(result)
运行一下:
=>> coffee async.coffee
Done 3s timeout
Result: "some result"
假装有两个进程
同样是上边的代码, 只是调整一下写法, 看上去像是分别启动了两个"进程",
虽然它们的运行时独立的, 但是可以通过管道进行通信,
而且在对应的 csp.take
和 csp.put
操作过程中, 会通过 yield 进行等待:
csp = require 'js-csp'
talk = (ch) ->
yield csp.timeout 3000
console.log 'Done 3s timeout'
yield csp.put ch, 'some result'
listen = (ch) ->
result = yield csp.take ch
console.log 'Result:', JSON.stringify(result)
# 创建 Channel, 启动两个"进程"
theCh = csp.chan()
# csp.go 后面第一个是 yield 函数, 第二个是参数的数组, 虽然比较难看
csp.go talk, [theCh]
csp.go listen, [theCh]
运行一下:
=>> coffee async.coffee
Done 3s timeout
Result: "some result"
封装异步事件
实际使用当中, 会需要把 js 环境的异步代码封装成管道的形式,
不封装成管道, 就不能借助 csp.go
来封装同步代码了,
由于 js 不像 Go 那样整个语言层面做了处理, 实际上会有奇怪的写法,
所以 js-csp 提供了 csp.putAsync
和 csp.takeAsync
:
csp = require 'js-csp'
talk = (ch) ->
setTimeout ->
csp.putAsync ch, 'some result'
console.log 'Finished 3s of async'
, 3000
listen = (ch) ->
result = yield csp.take ch
console.log 'Result:', JSON.stringify(result)
theCh = csp.chan()
talk theCh
csp.go listen, [theCh]
运行一下:
=>> coffee async.coffee
Finished 3s of async
Result: "some result"
处理超时
一个操作是否超时的问题, 可以同时启动一个定时的"进程",
然后观察两个"进程"哪一个先执行完成, 从而判断是否超时,
这就用到了 csp.alts
函数, 这个奇怪的命名是用 Clojure 带过来的:
csp = require 'js-csp'
talk = (ch) ->
time = Math.random() * 4 * 1000
setTimeout ->
console.log "Get result after #{time}ms"
csp.putAsync ch, 'some result'
, time
listen = (ch) ->
hurry = csp.timeout 2000
# 通过 csp.alts 同时等待多个 Channel 返回数据
result = yield csp.alts [ch, hurry]
# result.channel 可以用于判断数据的来源, result.value 才是真正的数据
if result.channel is hurry
console.log 'Too slow, got no result'
# close 只是设置 Channel 的状态, 其实还需要手工处理一些逻辑
hurry.close()
else
console.log 'Fast enough, got', JSON.stringify(result.value)
theCh = csp.chan()
talk theCh
csp.go listen, [theCh]
用了随机数, 运行多次试一下, 可以看到根据不同的时间, 结果是不一样的:
=>> coffee async.coffee
Too slow, got no result
Get result after 3503.6168682995008ms
=>> coffee async.coffee
Too slow, got no result
Get result after 3095.264637685924ms
=>> coffee async.coffee
Get result after 703.6501633183257ms
Fast enough, got "some result"
=>> coffee async.coffee
Too slow, got no result
Get result after 3729.5125755664317ms
=>> coffee async.coffee
Get result after 101.51519531067788ms
Fast enough, got "some result"
循环任务
跟 yield
用法类似, 如果有循环的代码, 也可以用 CSP 写出来,
这个的话不用怎么想应该能明白了, loop
只是 while true
的语法糖:
csp = require 'js-csp'
chatter = (ch) ->
counter = 0
loop
yield csp.timeout 1000
counter += 1
yield csp.put ch, counter
repeat = (ch) ->
loop
something = yield csp.take ch
console.log 'Hear something:', something
theCh = csp.chan()
csp.go chatter, [theCh]
csp.go repeat, [theCh]
运行一下:
=>> coffee async.coffee
Hear something: 1
Hear something: 2
Hear something: 3
Hear something: 4
^C
多个数据的消费者
实际场景当中会遇到多个消费者从单个生产者读取数据的需求,
这是一个用 Channel 比较合适的场景, 启动两个"进程"读取一个 Channel 就好了,
下面我模拟的是不同的处理时间 300ms 和 800ms 读取 100ms 频率的数据,
因为 CSP 自动处理了等待, 整个代码看上去挺简单的:
csp = require 'js-csp'
chatter = (ch) ->
counter = 0
loop
yield csp.timeout 100
counter += 1
yield csp.put ch, counter
repeat = (ch) ->
loop
yield csp.timeout 800
something = yield csp.take ch
console.log 'Hear at 1:', something
repeat2 = (ch) ->
loop
yield csp.timeout 300
something = yield csp.take ch
console.log 'Hear at 2:', something
theCh = csp.chan()
csp.go chatter, [theCh]
csp.go repeat, [theCh]
csp.go repeat2, [theCh]
运行一下:
=>> coffee async.coffee
Hear at 2: 1
Hear at 2: 2
Hear at 1: 3
Hear at 2: 4
Hear at 2: 5
Hear at 2: 6
Hear at 1: 7
Hear at 2: 8
Hear at 2: 9
Hear at 1: 10
Hear at 2: 11
Hear at 2: 12
Hear at 2: 13
Hear at 1: 14
Hear at 2: 15
Hear at 2: 16
Hear at 1: 17
Hear at 2: 18
Hear at 2: 19
Hear at 2: 20
Hear at 1: 21
Hear at 2: 22
Hear at 2: 23
Hear at 1: 24
^C
使用 buffer
默认情况下管道是阻塞的, csp.put
csp.take
成对进行,
也就是说, 只有一个就绪的话, 它会等待另一个开始, 然后一起执行,
但是用 buffer 的话, 管道就会先在一定范围内进行缓存,
这样 csp.put
就可以先运行下去了, 这个是不难理解的...
管道实际上有 3 种策略, fixed, dropping, sliding:
fixed, 缓存放满以后就会开始形成阻塞了
dropping, 缓存满了以后, 新的数据就会丢弃
sliding, 缓存满以后, 会丢弃掉旧的数据让新数据能放进缓存
随便演示一个丢弃数据的例子:
csp = require 'js-csp'
chatter = (ch) ->
counter = 0
loop
yield csp.timeout 200
counter += 1
console.log 'Write data:', counter
yield csp.put ch, counter
repeat = (ch) ->
loop
yield csp.timeout 300
something = yield csp.take ch
console.log 'Hear:', something
theCh = csp.chan(csp.buffers.dropping(3))
csp.go chatter, [theCh]
csp.go repeat, [theCh]
运行一下, 可以看到 "Hear" 部分丢失了一些数据, 但前三个数据不会丢:
=>> coffee async.coffee
Write data: 1
Hear: 1
Write data: 2
Hear: 2
Write data: 3
Write data: 4
Hear: 3
Write data: 5
Hear: 4
Write data: 6
Write data: 7
Hear: 5
Write data: 8
Hear: 6
Write data: 9
Write data: 10
Hear: 7
Write data: 11
Hear: 8
Write data: 12
Write data: 13
Hear: 9
Write data: 14
Hear: 11
Write data: 15
Write data: 16
Hear: 12
Write data: 17
Hear: 14
^C
小结
由于 CSP 是在 Go 语言发明的, 完整的用法还是看 Go 的教程比较好,
到了 Clojure 和 js 当中难免会增加一些坑, 特别是 js 当中...
上面提到的 API 在 js-csp 的文档上有描述, 例子也有, 但是挺少的:
https://github.com/ubolonton/...
https://github.com/ubolonton/...
另外还有一些高级一点的用法, 比如数据的 transform 和 pipe 之类的,
其实就是 Stream 的用法在 Channel 上的改版, 某种程度上 Channel 也是 Stream,
对于我个人来说, Channel 的抽象比起 Stream 的抽象舒服多了.