九 月
21
金曜日

core.asyncのtimeoutについて

Clojure core.asynctimeoutに関するメモ。


時間経過で自動クローズするチャネル

timeout 関数で作成したチャネルは指定の時間経過で自動的にクローズする。
当該チャネルに対する読み込み操作はチャネルがクローズするまで block または park されるため、

一時停止や、

(require '[clojure.core.async :as ca])
(ca/go
  (prn "5秒間待つ...")
  (ca/<! (ca/timeout 5000))
  (prn "Hello!"))

alts!alt!を併用したタイムアウト処理に利用することができる。

;; チャネルc1から値を読み取る
;; 1秒以内に読み取りができなければタイムアウトさせる
(ca/alts! [c1 (ca/timeout 1000)])

また時間経過の起点は読み込み時ではなく、チャネル作成時となる。
例えばtimeoutチャネル作成時から読み込み操作までの間に時間が空くとその分タイムアウト時間が短くなる。

(let [timeout-ch (ca/timeout 5000)]
  ;; 4秒かかる何かの処理
  (do-something)
  ;; 既に4秒経過しているため1秒しか一時停止しない
  (ca/<! (ca/timeout timeout-ch))
  (prn "Hello!"))

共有の可能性

timeout関数で作成されたチャネルはクローズ予定時刻のエポックミリ秒をキーにキャッシュされる。
キャッシュされたチャネルは同一スレッド内や別スレッド、別IoCスレッド間で共有される可能性がある。

例えば以下のように5秒後にタイムアウトするtimeoutチャネルc1とc2を作成した場合、
c1とc2には同一のチャネルオブジェクトが束縛されている。

(let [c1 (ca/timeout 5000)
      c2 (ca/timeout 5000)]
  (identical? c1 c2))
;=> true

この挙動は timeout 関数のコード clojure/core/async/impl/timers.clj で確認することができる。

(defn timeout
  "returns a channel that will close after msecs"
  [^long msecs]
  (let [timeout (+ (System/currentTimeMillis) msecs)
        me (.ceilingEntry timeouts-map timeout)]
    (or (when (and me (< (.getKey me) (+ timeout TIMEOUT_RESOLUTION_MS)))
          (.channel ^TimeoutQueueEntry (.getValue me)))
        (let [timeout-channel (channels/chan nil)
              timeout-entry (TimeoutQueueEntry. timeout-channel timeout)]
          (.put timeouts-map timeout timeout-entry)
          (.put timeouts-queue timeout-entry)
          timeout-channel))))
  • timeoutチャネルを作成する際、チャネルのクローズ予定エポックミリ秒以降の最も近いキャッシュを取得
  • 取得したチャネルのクローズ予定時刻と自分のクローズ予定時刻が10ミリ秒以内であれば同チャネルを使用
  • キャッシュにヒットしなければ新規にtimeoutチャネルを作成、クローズ予定時刻のエポックミリ秒をキーにキャッシュに登録

なおtimeoutチャネルを普通に利用していれば共有の弊害が出ることはないので気にする必要は全くない。

明示的なクローズ

timeoutチャネルに対して明示的に close! を行ったらどうなるのか?
勿論してはならないし、する必要もないが、もし したらどう動くのかを確認してみた。

結果:残り時間を待たずに即時クローズする

この挙動は一見すると何かに利用できそうな気がするが、前段のtimeoutチャネルの共有の可能性があるため絶対に行ってはならない。

書き込み

timeoutチャネルに対して書き込み操作を行ったらどうなるのか?
絶対しない操作でありこの行為に思いを馳せる事自体無駄である感は拭えないがせっかくなので確認してみた。

結果:バッファなしのチャネルと同じ扱い

なのでtimeoutチャネルを読み込む側がいなければ永遠に block または park。
読み込み側がいれば書き込んだ値が読み込まれるが、読み込み側としてはタイムアウトを待たずにnil以外の値が取れてしまうので処理としては破綻。