チャネルを作成するときにチャネルを使用するパターンは、ますます人気を集めています。
マルチスレッドアプリケーション。 アイデアは新しいものではなく、そのデザインは1978年に撤回されました。
最も有名な実装は現在Golangで広く使用されています。
この記事では、Clojureのcore.asyncでのCSPの実装について検討します。興味があれば、catにようこそ。
この記事では、この記事で説明されているcore.asyncを操作するための簡単で基本的なプラクティスについて説明します。マルチスレッドプログラミングを始めるには十分です。
Golangとは異なり、チャネルを介してストリームを操作するパラダイムは言語自体に組み込まれていますが、core.asyncは単なるClojureのライブラリです。別のパラダイムが好きな場合は、 pulsar 、 promesa 、 manifoldの選択肢があります
同時に、core.asyncとpromesaはClojureScriptのブラウザー側でも使用できます。この場合、当然、このようなものはすべてES5でコンパイルされ、ブラウザーで実行されるため、マルチスレッドについて話す必要はありません。よく役立つ。
それで、core.asyncは何を提供しますか? 指で説明すると、core.asyncは8つのスレッドで構成される固定スレッドプールへのgo-blocksによるスケジューリングを提供します(スレッドプールのサイズは特別なオプションで変更できます)。 メッセージがチャネルに到着すると、core.async自体が空きストリームを見つけてタスクを渡すか、メッセージをキューに入れます。 スレッドプールについて最初に聞いた人は、 ワーカースレッドパターンに関する良いメモを読むことができます。
例1
(defonce log-chan (chan)) (defn loop-worker [msg] (println msg)) (go-loop [] (let [msg (<! log-chan)] (loop-worker msg) (recur)))
上記の例では、 log-chan
チャネルを作成し、チャネルからのメッセージを処理するループワーカー関数を定義し、ループワーカーを配置して無限ループのgo-blockを作成しましたloop-worker
これで、チャネルにデータを送信できますloop-worker
(>!! log-chan "")
ループワーカー関数は、REPLによる便利なデバッグのために、go-blockに対して意図的に個別に渡されました。
このマクロはcore.async内のどこかでベイク処理され、REPLでオンザフライで再コンパイルされるので、go-loop本体自体が奇妙であるため、個別に取り出して安心して生活するのが簡単です。
ここで、go-loopは通常の理解では無限ループを実行しないことに注意してください。
メッセージを受信した後、ハンドラー関数の1回限りの実行が発生し、その後<!
新しいメッセージを待ちます。 したがって、チャンネルとハンドラーをいくつでも作成できます。
go-block内では、チャンネルからの読み取り機能<!
ストリームを駐車します。
goブロックの外では、関数<!!
を使用できます<!!
メッセージを受信するまでメインスレッドをブロックします。 動作<!!
ES7のawait機能と比較できます。
パーキングゴーブロック。これはcore.asyncという用語で、スレッドが解放され、他のタスクに使用できることを意味します。 ブロックという用語もあります。つまり、スレッドは直接ブロックされ、新しいタスクがリリースされるまでアクセスできなくなります。
例#1に問題があります。ループワーカーでException
(recur)
した場合、フォームは中断され(recur)
繰り返し(recur)
呼び出されないため、 log-chan
チャネルからのデータの待機は停止します。これを例#2で修正します。
例2
(defonce log-chan (chan)) (defn loop-worker [msg] (throw (Exception. "my exception message"))) (go-loop [] (let [msg (<! log-chan) res (try (loop-worker msg) :ok (catch Exception e (println (.getMessage e)) :error))] (recur)))
この例では、ループワーカー呼び出し全体をtry
フォームにラップし、 res
変数にはフォームが正常に完了したかエラーかを示すフラグが含まれます。 このフラグは、たとえば、エラーが発生した場合にチャネルを閉じたい場合に便利です。 このアプローチの実際の例はこちらにあります。
例3
(let [c1 (go (<! (timeout (rand-int 1000))) 5) c2 (go (<! (timeout (rand-int 1000))) 7)] (go (let [v1 (<! c1) v2 (<! c2)] (println {:v1 v1 :v2 v2 :summ (+ v1 v2)}))))
この例では、 let
ブロックにリストされているすべての非同期操作の結果を待ちます。 この方法は、JavaScriptコールバックの地獄の問題を解決するのに非常に便利であり、ClojureScriptに直面してブラウザー側で使用できることを嬉しく思うもう1つの理由です。
例4
(defn upload "upload emulator" [headshot c time] (go (Thread/sleep time) (>! c headshot))) (let [c1 (chan) c2 (chan)] (upload "pic1.jpg" c1 30) (upload "pic2.jpg" c2 40) (let [[headshot channel] (alts!! [c1 c2 (timeout 20)])] (if headshot (println "Sending headshot notification for" headshot) (println "Timed out!"))))
この例では、非同期操作(この場合はファイルのアップロード)をエミュレートするアップロード関数を作成しました。 アップロードする最後の引数は、ミリ秒単位の遅延時間です。 関数altsの使用!!! 最初の結果を取得できます。ベクトルにリストされているチャネルのいずれかが返されます。 ベクターでは、最後のチャネルが(timeout 20)
、このチャネルは20ミリ秒後に結果を返します。これがheadshot
変数に書き込まれる最初の値になり、フォームの実行が続行されます。 したがって、この例では、タイムアウトの時間の設定をエミュレートします。その間、非同期操作のセットが完了するまで待機します。
例5
(def ping (chan)) (def pong (chan)) (go-loop [] (let [msg (<! ping)] (when (= msg :ping) (println msg) (>! pong :pong) (Thread/sleep 1000)) (recur))) (go-loop [] (let [msg (<! pong)] (when (= msg :pong) (println msg) (>! ping :ping) (Thread/sleep 1000)) (recur))) (>!! ping :ping)
2つのチャネル間の通信の例、古典的なピンポン。
これは私が最後に見せたかった例です。 それとは別に、複数のストリームに情報を記録するために特別に作成されたデータ型のクロージャー内の存在を強調する価値があります。これらは、 原子およびエージェントであり、他の型の一般的な不変性であり、これはすべて、マルチスレッドアプリケーションを開発する際の開発者の生活を大幅に簡素化します。
便利なリンク:
»http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html
»https://github.com/clojure/core.async
»https://github.com/clojure/core.async/wiki/Getting-Started
»Http://www.braveclojure.com/core-async/
»http://go.cognitect.com/core_async_webinar_recording