源文件是
core.async
倉庫的一個程式碼檔案, 包含大量的教程性質的註釋
https://github.com/clojure/core.async/blob/master/examples/walkthrough.clj
中間不確定的兩句留了原文, 有讀懂的同學請回復幫我糾正
這份攻略介紹 core.async
核心的一些概念
clojure.core.async
namespace 包含了公開的 API.
(require '[clojure.core.async :as async :refer :all])
Channel
資料通過類似佇列的 Channel 來傳輸, Channel 預設不進行 buffer(長度為 0)
需要生產者和消費者進行約定從而在 Channel 當中傳送資料
用 chan
可以建立一個不進行 buffer 的 Channel:
(chan)
傳一個數字以建立限定了 buffer 大小的 Channel:
(chan 10)
close!
用來關閉 Channel 終結接受訊息傳入, 已存在的資料依然可以取出
取盡的 Channel 在取值時返回 nil
, nil
是不能直接通過 Channel 傳送的!
(let [c (chan)]
(close! c))
一般的 Thread
對在一般的 Thread 中, 使用 >!!
(阻塞的 put) 和 <!!
(阻塞的 take)
與 Channel 進行通訊
(let [c (chan 10)]
(>!! c "hello")
(assert (= "hello" (<!! c)))
(close! c))
由於是這些呼叫是阻塞的, 如果嘗試把資料放進沒有 buffer 的 Channel, 那麼整個 Thread 都會被卡住.
所以需要 thread
(好比 future
) 線上程池當中執行程式碼主體, 並且通過 Channel 傳回資料
例子中啟動了一個後臺任務把 "hello"
放進 Channel, 然後在主執行緒讀取資料
(let [c (chan)]
(thread (>!! c "hello"))
(assert (= "hello" (<!! c)))
(close! c))
go
程式碼塊和反轉控制(IoC) thread
go
是一個巨集, 能把它的 body 在特殊的執行緒池裡非同步執行
不同的是本來會阻塞的 Channel 操作會暫停, 不會有執行緒被阻塞
這套機制封裝了事件/回撥系統當中需要外部程式碼的反轉控制
在 go
block 內部, 我們使用 >!
(put
) 和 <!
(take
)
這裡把前面 Channel 的例子轉化成 go
block:
(let [c (chan)]
(go (>! c "hello"))
(assert (= "hello" (<!! (go (<! c)))))
(close! c))
這裡使用了 go
block 來模擬生產者, 而不是直接用 Thread 和阻塞呼叫
消費者用 go
block 進行獲取, 返回 Channel 作為結果, 對這個 Channel 做阻塞的讀取
(原文: The consumer uses a go block to take, then returns a result channel, from which we do a blocking take.)
選擇性(alts
)
Channel 對比佇列一個啥手機應用是能夠同時等待多個 Channel(像是 socket select)
通過 alts!!
(一般 thread)或者 alts!
(用於 go
block)
可以通過 alts
建立後臺執行緒講兩個任意的 Channel 結合到一起alts!!
獲取集合中某個操作的來執行
或者是可以 take
的 Channel, 或者是可以 put
[channel value]
的 Channel
並返回包含具體的值(對於 put
返回 nil
)以及獲取成功的 Channel:
(原文: alts!!
takes either a set of operations to perform either a channel to take from a [channel value] to put and returns the value (nil for put) and channel that succeeded:)
(let [c1 (chan)
c2 (chan)]
(thread (while true
(let [[v ch] (alts!! [c1 c2])]
(println "Read" v "from" ch))))
(>!! c1 "hi")
(>!! c2 "there"))
列印內容(在 stdout, 可能你的 REPL 當中看不到):
從 #<ManyToManyChannel ...>
讀取 hi
從 #<ManyToManyChannel ...>
讀取 there
使用 alts!
來做和 go
block 一樣的事情:
(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")))
因為 go
block 是輕量級的程式而而不是限於 thread, 可以同時有大量的例項
這裡建立 1000 個 go
block 在 1000 個 Channel 裡同時傳送 hi
它們妥當時用 alts!!
來讀取
(let [n 1000
cs (repeatedly n chan)
begin (System/currentTimeMillis)]
(doseq [c cs] (go (>! c "hi")))
(dotimes [i n]
(let [[v c] (alts!! cs)]
(assert (= "hi" v))))
(println "Read" n "msgs in" (- (System/currentTimeMillis) begin) "ms"))
timeout
建立 Channel 並等待設定的毫秒時間, 然後關閉:
(let [t (timeout 100)
begin (System/currentTimeMillis)]
(<!! t)
(println "Waited" (- (System/currentTimeMillis) begin)))
可以結合 timeout
和 alts
來做有時限的 Channel 等待
這裡是花 100ms 等待資料到達 Channel, 沒有的話放棄:
(let [c (chan)
begin (System/currentTimeMillis)]
(alts!! [c (timeout 100)])
(println "Gave up after" (- (System/currentTimeMillis) begin)))
ALT
todo
其他 Buffer
Channel 可以定製不同的策略來處理 Buffer 填滿的情況
這套 API 中提供了兩個實用的例子.
使用 dropping-buffer
控制當 buffer 填滿時丟棄最新鮮的值:
(chan (dropping-buffer 10))
使用 sliding-buffer
控制當 buffer 填滿時丟棄最久遠的值:
(chan (sliding-buffer 10))