Clojureの並行プログラミングについて.
Clojure: Parallelism
スレッド, バックグラウンド実行など.
clojure.core/future
https://clojuredocs.org/clojure.core/future
Java Futureを内部で利用.
すべての要素に対してfutureを作成するということは, clojure.lang.Agent/soloExecutorとよばれるキャッシュ化スレッドプールをつかう. Executors.newCachedThreadPoolで作成される非同期スレッドプール. 使わなくなったスレッドは60秒で破棄.
clojure.core/pmap
pmapはclojure.core/futureをつかうのでfutureの派生と考えればいい.
pmapは遅延シーケンスを構築するものの, doallで評価するとリストがパラレルに並列実行されて結果がリストで返される.
ref. pmap - clojure.core | ClojureDocs
以下の2つでは実行時間に4倍の差が生まれる.
(defn long-running-job [n]
(Thread/sleep 3000)
(+ n 10))
(doall (map long-running-job (range 4)))
(doall (pmap long-running-job (range 4)))
- refs.
- Idiomatic Clojure: Mixing Side Effects and Iteration
- Idiomatic Clojure: Mixing Parallel Side Effects and Iteration
clojure.core/promise
Clojure Concurrent Library
🔧funcool/promesa
JavaScriptの📝Promise(JS)のように非同期処理を記述する方法. ClojureはCSPモデル(core.async)が有名なのでどちらかというとasync/awaitのような記述方法がマイナー.
https://github.com/funcool/promesa
- Clojure/ClojureScript両方対応.
- 🔧nbbはこれがdefaultで組み込まれていて活用している.
- 💡CLJS/JS Promise(async/await) Interop
⚖core.async vs Manifoldの比較
core.asyncとmanifoldは同じことができる.
- What is the difference between Manifold and core.async? : Clojure
- Question on Manifold / Aleph - How to? - ClojureVerse
core.async main abstractions are channels (and goblocks); manifold main abstractions are deferred and stream.
元々は異なる目的のために開発されたが, 0.2.2でlet-flowとgo-offが入ったことによってcore.asyncのgoをミラーしてつかうような機能が入ってgoblocksの機能をcore.asyncから借りることが出来るように改造された.
Manifoldの先行きのメンテナンス状況が不安.
;; https://twitter.com/iku000888/status/1274606555430674434
manifoldは作者がclojureから去ったのとclojure界隈でも使われているのはレアだと思うのでやめといた方がという気持ちです。
勝手なわたしの解釈だと, データフロー変数及びそれをリストにした遅延シーケンスをコアにする📝データフロープログラミングのパラダイムにあるのがManifoldであり, 📝CSPモデルを再現したのがcore.asyncであり, 出発点が結構違う.
go-offのdotstringにより詳細があった.
https://cljdoc.org/d/manifold/manifold/0.2.4/api/manifold.go-off
go-offは必要に応じてcore.asyncをつかうがThreadPoolに空きがあれば必要ないので使わないようだ.
`core.async/go` assumes that all of your code is able to be purely async and will never block the handling threads. `go-off` removes the concept of handling threads, which means blocking is not an issue, but if you spawn too many of these, you can create more threads than the OS can handle.
Clojure Concurrency Topics
💡性能のボトルネックがCPUバウンドかIOバウンドか - Clojure
- Clojureのいろんな並行処理の使い分け - 紙箱
- concurrency - Clojure: future, agent or core.async for IO - Stack Overflow
このへんの議論はGoのgoroutineで検索をかけてもいいかも. Clojureよりも情報が圧倒的に多い. しっかりやるならば, 計測して実験をする必要がある.
💡固定数スレッドプールとキャッシュ化スレッドプール
Clojureにはいろいろな並行処理がある.
- agent系(send/senf-off)
- future系(pmap)
- core.async系(go/thread)
切り分けのポイントは, CPU Bound vs IO Bound(💡性能のボトルネックがCPUバウンドかIOバウンドか)となるが, さらに突っ込むと使用しているスレッドプールの議論になる.
CPU Bound | IO Bound |
---|---|
固定数スレッドプール | キャッシュ化スレッドプール |
Executors.newFixedThreadPool | Executors.newCachedThreadPool |
Clojure並列処理の改良外部ライブラリはいろいろあるが, つまるところここにいきつく.
さらに, デフォルトClojureの仕組み(send/send-off)か, ライブラリ独自管理のスレッドプールかで冗長化どうかも考慮が必要. できればいろんなライブラリを使わずに共通のスレッドプールで管理したいところ.
以下の記事がとてもまとまっている.
ref. Clojureのいろんな並行処理の使い分け - 紙箱
💡接続待機とpromise
なるほどと思ったpromiseの使い方メモ.
clientからのwebsocketの接続処理で利用している. 呼び出し前でpromiseの値を作成して, callback関数の中でpromiseに対してdeliverをする. 呼び元の関数ではderefで待機する.
ここでやっていることは, promiseの値は非同期で動く真偽値のフラグであるが, callback関数でdeliverによって値がbindされるまではスレッドをブロッキングすることによって関数を即時リターンさせずにconnect完了したらリターンする.
現在動作しているスレッド名とスレッドIDの取得
JavaのThradクラスのIFを活用する.
(.getId (Thread/currentThread))
(.getName (Thread/currentThread))
Clojure Concurrency Insights
⚖Clojure: atom/delay/future/promiseの比較
いずれもClojure: derefという同じ関数で読み出せたり, realized?で呼び出したか確認できるところがややこしい.
- atom: メモリ共有
- スレッド内のメモリ共有の仕組みあり状態を扱う.
- 💡Clojure atomはロックフリー, でありdelay/future/proimceはロックする.
- delay: 遅延評価
- force or deref or @ をつかって評価してはじめて計算がはじまる.
- future: バックグラウンド実行
- 評価はバックグラウンド(i.e.別スレッド)で始まるが, その結果は参照してはじめてわかる (or まだ計算が終わってないかもということもわかる).
- promise: バックグラウンド実行 + 遅延評価.
- promiseはfutureに似ている.
- どちらもバックグラウンド実行に対する参照を返す.
- futureは宣言時にプロシージャも宣言して即時実行.
- promiseは宣言時は参照に過ぎずdeleverで処理をあとからbindingする.
- promiseは値, futureは関数値のようなイメージ.
- どちらもバックグラウンド実行のためderefで結果を参照できる.
- promiseはfutureに似ている.
- refs.
- links
🤔Clojureのpromise/deliverはデータフロー変数のこと
Promiseはbindされるまでスレッドを止める. これはまさにデータフロープログラミングパラダイムにおける📝データフロー変数のことである.
たまたま過去メモを復習していたら気づいたけど, 言われてみないとDocumentに書いてなければそれがなんなのかわからない…
- refs.
- links.
🔗References
- 値と変化—Clojureにおける値のアイデンティティとステート - Rich Hickey(2009)
- Clojureがなぜ📝Actor モデルを採用しなかったか.
- GitHub - melissavoegeli/threading-in-clojure
- 非同期と状態のよいオンラインドキュメント.