[譯]介紹 `core.async` 核心的一些概念

題葉發表於2016-01-15

源文件是 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)))

可以結合 timeoutalts 來做有時限的 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))

相關文章