SlideShare a Scribd company logo
ふつうのプログラムで
使うcore.async
2018/5/18 矢野勉
core.async使ってますか
?
まとまっとドキュメントがない
いい感じのチュートリアルがない
注意すべきポイントが共有されてない
ノウハウが共有されていない
というわけで
基礎と使い所の話
入門編
;; goブロック内は非同期に動く
;; 非同期ブロック内でチャンネルにデータを書く
(go
(>! ch data))
;; 別のgoブロックで同じチャンネルをreadする
;; こちらも別の非同期処理として動く
(go-loop
(when-some [data (<! ch)]
(do-action data)
(recur)))
基本形
goブロックで非同期処理を作り、その中でチャンネルに書いたり読んだりする。
全てのgoブロックはcore.async管理下のスレッドにより効率的に切り替わる
go/thread/recur
go
IO待ちが発生しない処理に使います。
goの中でIO待ちが発生する処理(API呼び出しやDBアクセス、ファイルアクセス)
が発生する場合は、その部分をさらにthreadで囲むのが理想的です。
固定数のスレッドプールを使うので、一度に動けるgoブロックの数は制限されます
。ちゃんと切り替えることが重要。
thead
IO待ちなどブロックするしょりに使います。
キャッシュ化スレッドプールを使って起動されるので、プールを使い切るというこ
とがありません
go/thread/recur
go/thread/recur
go-loop
(go
(loop
処理)
の、単なる省略形です。
「動き続ける処理は必ずgo-loopを使う」というのはよくある勘違いなので注意。go
やthreadのなかでloopすればなんでもいいんです。
recur
goブロックは単なる非同期ブロックなので、自分でループしないと、すぐに死にま
す。
たまに、recurを忘れて「軽量プロセスなのになんですぐ落ちるんだ」と悩む人がい
るそうです。
go/thread/recur
goやthreadのなかでloop/recurして、はじめて軽量プロセス的なものになるのであっ
て、逆ではありません。
chan
チャンネルの作成関数だけど、引数が結構あるので、ちゃんと知っておくと便利
chan/buffer
(chan (buffer 10) ;バッファ
(filter #(not= (:type %) :error) ; transducer
#(log/error % “an error occurred.”)) ; 例外ハンドラ
buffer
チャンネルへのアクセスバッファ。
transducerを指定する場合は、最低でも1を指定する必要がある
chan/buffer
チャンネルアクセスでプログラムが停止するのを抑止するキーファクターです。
とても重要。
十分な数のバッファを指定するか、sliding-bufferやdropping-bufferのような、溢れた
場合のアクションが付いているバッファの使用を検討しましょう
timeout
一定時間経つと値が取れるだけのチャンネルを作る関数です。
timeout/alt!/alts!
基本的にalt!と組み合わせて使いますが、一定時間ごとに処理を行うループを作ると
きにも使います。
(go-loop
;; なにかアクション
(<! (timeout 5000)) ; 5秒停止。
;; チャンネルアクセスを含むので他のブロックに切り替わる
(recur))
alts!/alts!!
core.asyncのキラー機能の一つ。
timeout/alt!/alts!
複数のチャンネルを渡すと、一番最初に結果が取れるようになったチャンネルを返
します。
alts!/alts!!
timeout/alt!/alts!
(go
(let [amazon-ch (access-to-amazon args)
google-ch (access-to-google args)
[value ch] (alts! [amazon-ch google-ch])]
(cond
(= ch amazon-ch)
(println “amazon result:” value)
(= ch google-ch)
(println “google result:” value))))
書くこともできます
timeout/alt!/alts!
(go
(let [[result ch] (alts! [[amazon-ch send-value]
[google-ch send-value]])]
(cond
(= ch amazon-ch)
(println “amazon result:” result)
(= ch google-ch)
(println “google result:” result))))
最初に書き込み完了したチャンネルと値が帰ってきます
書き込み処理の結果は、かけたかどうかだけなので、true/falseのいずれかです
alt!/alt!!
timeout/alt!/alts!
(go
(let [v (alt!
[read-ch]
([v ch] (log/info “value=“ v) v)
[(timeout 5000)]
([v ch] (log/warn “Timeout!!“) nil))]
;; なんか処理))
便利マクロ。「どのチャンネルか?」のチェックが不要なので使いやすいです。
timeoutと組み合わせて使うことで、チャンネルへのアクセスにタイムアウトを設定
できるので、よく使います。
alt!/alt!!
timeout/alt!/alts!
(go
(let [v (alt!
[[write-ch value]] :sent
[(timeout 5000)] :timeout)]
(case v
:sent
(log/info “sent!!”)
:timeout
(log/warn “Send timeout!!”))))
書き込みタイムアウトを指定したい場合にも使えます。
タイムアウト重要!
すべてのチャンネル操作に指定する必要はないが、アプリがチ
ャンネル操作を開始する操作には、必ず指定すべき。
だから alt!/alt!! はほぼ必須です
IOがすごく遅延することもあるし、プログラムミスでチャンネ
ル同士が互いに待つ(デッドロック)を生み出してしまうこと
もあります
タイムアウトが設定されていれば、その部分でちゃんと落ちて
くれます。
mult, tap
いざというとき役立つやつ
ひとつのチャンネルを複数に分割して、すべての分割チャンネルに同じデータが流
れる構造を作れます
いざというとき役立つやつ
pub, sub
一つのチャンネルから「交付(publication)」を作り、それに「購読(subscript)」する
ことで、あるチャンネルから条件に合致するデータだけを受け取る別のチャンネル
を作り出せます
pipelineに乗せろ
• core.asyncの非同期ブロックを複数個起動した時の処理は
結構めんどくさい
• 非同期ブロックで起きた例外はどうするのか、とか
• pipelineに乗せると少し楽になる
大きく3種類
pipeline
pipeline-blocking
pipeline-async
CPUを使い切る処理
(ブロックしない)
ブロックする処理
非同期ブロックの管理はま
かせる pipeline pipeline-blocking
非同期ブロックの選択も起
動も自分で行う pipeline-async
pipeline
pipeline-blocking
平行数とTransducerを指定すると、チャンネルに書くたびに並列でTransducerが
実行されます。
(pipeline 5 out-ch (map string/upper-case) in-ch true ex-handler)
in-chに入ってきた順番で、out-chに結果が入っていくことが保証されます。
transducerで例外が発生すると、ex-handlerに渡した関数が呼び出されます。
第5引数のbooleanがtrueだと、in-chが閉じるとout-chも閉じます。
pipeline-async
transducerの代わりに、in-chに入ってきた値と、結果出力用のチャンネルの2引数
を受け取る関数を指定します。
非同期ブロック(goやthread)の起動は、関数内で自分で行います。
(pipeline 5 out-ch (fn [v ch] (go (>! ch :result))) in-ch true ex-handler)
こちらも、in-chに入ってきた順番で、out-chに結果が入っていくことが保証され
ます。
transducerで例外が発生すると、ex-handlerに渡した関数が呼び出されます。
データによってgoとthreadを使い分けたり、複数の非同期ブロックを起動したり
できます。
並列数が指定できるので
• pipeline-blockingなら、API呼び出しの同時アクセス数を
制限することができる(pmapにはできない)
• pipelineなら、同時処理数をCPUコア数に制限することが
できる
実践編
APIコールを並列化して効率
化したいなあ
• pmapなら簡単に並列化できる
• でも、pmapは引数のコレクションの要素数だけスレッ
ド起動しちゃうので、相手のAPIに大量のアクセスがい
ってしまうかも。。
pipeline-blockingでできる
(defn post-data-to-api
[data-coll]
(let [in-ch (chan 1)
out-ch (chan 1)]
(pipeline-blocking
5
out-ch
(map #(call-api %)) ;;transducer
in-ch
true
(fn [ex] (log/error ex “Error!!”) nil))
(onto-chan in-ch data-coll) ;; in-chに全部入れる
;; 以下、out-chを読み込む処理
))
アプリ全体でAPIコールの並
列数を制限したい
• Webサーバとかだと、たくさんリクエストがきても、API
へのアクセス数は「全体で最大10並列」とかに制限した
いですよね。
• 毎回pipelineを作ると、pipeline単位で10並列になるだ
け。
pipelineを閉じなけれ
ばいい
ただしpipelineを閉じないと
いうことは
• transducerに渡ってくるデータは、たくさんのリクエスト
がまざったものになります。
Pipeline
ここが並列にな
る
データにIDをつける
(defn transaction-id
[]
(str (UUID/randomUUID)))
(defn make-request
[id data]
{:transaction-id id
:data data})
pub/subで自分のIDのデータ
だけに絞る
;; out-chから、:transaction-idで購読できるpublicationをつくる
(def publication (pub out-ch :transaction-id))
;; 自分のIDを指定して購読
(let [my-id-ch (chan 10)]
;my-id-chには、:transaction-idがidと
;合致するデータだけが流れ込んできます
(sub publication id my-id-ch)) ;idは自分のtransaction-id
pub関数、sub関数を使うと、チャンネルから取れるデータのうち、条件に合う
データだけが読める、別のチャンネルを作ることができます。
これを利用して、自分のtransaction-idと同じデータだけを読めるチャンネルを
作り出します。
終端データを決める
(defn make-end-data
“:data部が ::end であるデータを終端とみなす”
[id]
{:transaction-id id
:data ::end})
;; 例
(go-loop []
(when-let [data (<! ch)]
(let [value @data]
(when (not= ::end (:data value))
(my-action)
(recur)))))
pipelineが閉じない=チャンネルも閉じない、ということなので、
「チャンネルが閉じるまで読み続ける」という定番ループは作れない
なので、データを入れる時に終端データを入れる。読むときは
「終端データが来るまで読む」というループにする
平行化するだけじゃなく、コレクションを
入れたらコレクションで結果がほしいな
• 使う側にとってはコレクションを関数に渡したら、勝手
に並列で処理が走って、結果もコレクションになってて
ほしい。
• ちょうどpmapがやってくれるような形にしたい
lazy-seqと組み合わせる
;;; pipelineのout-chがすでにある前提
;;; 全部処理が終わったら、out-chも閉じるはずなので、それまで再帰する
;;; 遅延シーケンスを作れば良い
(letfn [(make-lazy-seq [ch]
(lazy-seq
(if-some [v (<!! ch)]
(cons v (make-lazy-seq ch))
[])))] ;チャンネルが閉じたので再帰しない。
(make-lazy-seq out-ch))
非同期処理で起きた例外をメイ
ンスレッドでキャッチしたい
• 例外ハンドラは非同期スレッド内で呼ばれるので、アプ
リ全体のグローバルな例外処理に例外が渡らなくて困る
ことがある
チャンネルに渡す値はbox化する
いくつかのオープンソースのライブラリでも同様の方法でチャン
ネルを扱っています
チャンネルにはnilをかきこむことはできませんが、box化すると、
処理の結果値としてnil値を返すこともできるので便利です。
atomやfutureのように、derefで値を取り出せるような何かに、実
際の値をラップします。
deref時に、値が例外であれば例外をスローするように実装します
。
;; boxを作る関数
(defn box
[value]
(reify
clojure.lang.IDeref
(deref [this]
(if (instance? Throwable value)
(throw value)
value))))
;; 関数を実行して結果をboxにして返す関数。
;; 例外発生時は例外をラップしたboxを返す
(defn do-action
[f input-data]
(try
(box (f input-data)) ; 関数の結果をbox化する
(catch Throwable ex
(box ex)))) ;例外発生時も単にbox化する
;; 読む側は、単にderefすれば、読んだスレッドで
;; 勝手に例外がスローされる
(loop []
(when-some [result (<!! ch)]
(let [value @result]
;; なにかvalueを使った処理
(recur))))
非同期処理を使うと
大抵使うテクニックなので、
できればライブラリ化したい
おわり

More Related Content

What's hot

What's hot (20)

形式手法と AWS のおいしい関係。- モデル検査器 Alloy によるインフラ設計技法 #jawsfesta
形式手法と AWS のおいしい関係。- モデル検査器 Alloy によるインフラ設計技法 #jawsfesta形式手法と AWS のおいしい関係。- モデル検査器 Alloy によるインフラ設計技法 #jawsfesta
形式手法と AWS のおいしい関係。- モデル検査器 Alloy によるインフラ設計技法 #jawsfesta
 
ゲーム開発者のための C++11/C++14
ゲーム開発者のための C++11/C++14ゲーム開発者のための C++11/C++14
ゲーム開発者のための C++11/C++14
 
目grep入門 +解説
目grep入門 +解説目grep入門 +解説
目grep入門 +解説
 
Laravel × レイヤードアーキテクチャを実践して得られた知見と反省 / Practice of Laravel with layered archi...
Laravel × レイヤードアーキテクチャを実践して得られた知見と反省 / Practice of Laravel with layered archi...Laravel × レイヤードアーキテクチャを実践して得られた知見と反省 / Practice of Laravel with layered archi...
Laravel × レイヤードアーキテクチャを実践して得られた知見と反省 / Practice of Laravel with layered archi...
 
PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門PlaySQLAlchemy: SQLAlchemy入門
PlaySQLAlchemy: SQLAlchemy入門
 
強いて言えば「集約どう実装するのかな、を考える」な話
強いて言えば「集約どう実装するのかな、を考える」な話強いて言えば「集約どう実装するのかな、を考える」な話
強いて言えば「集約どう実装するのかな、を考える」な話
 
Dockerfileを改善するためのBest Practice 2019年版
Dockerfileを改善するためのBest Practice 2019年版Dockerfileを改善するためのBest Practice 2019年版
Dockerfileを改善するためのBest Practice 2019年版
 
テスト文字列に「うんこ」と入れるな
テスト文字列に「うんこ」と入れるなテスト文字列に「うんこ」と入れるな
テスト文字列に「うんこ」と入れるな
 
"Simple Made Easy" Made Easy
"Simple Made Easy" Made Easy"Simple Made Easy" Made Easy
"Simple Made Easy" Made Easy
 
Domain Driven Design with the F# type System -- F#unctional Londoners 2014
Domain Driven Design with the F# type System -- F#unctional Londoners 2014Domain Driven Design with the F# type System -- F#unctional Londoners 2014
Domain Driven Design with the F# type System -- F#unctional Londoners 2014
 
Linuxにて複数のコマンドを並列実行(同時実行数の制限付き)
Linuxにて複数のコマンドを並列実行(同時実行数の制限付き)Linuxにて複数のコマンドを並列実行(同時実行数の制限付き)
Linuxにて複数のコマンドを並列実行(同時実行数の制限付き)
 
これからの「async/await」の話をしよう
これからの「async/await」の話をしようこれからの「async/await」の話をしよう
これからの「async/await」の話をしよう
 
REST API のコツ
REST API のコツREST API のコツ
REST API のコツ
 
クロージャデザインパターン
クロージャデザインパターンクロージャデザインパターン
クロージャデザインパターン
 
Go初心者がGoでコマンドラインツールの作成に挑戦した話
Go初心者がGoでコマンドラインツールの作成に挑戦した話Go初心者がGoでコマンドラインツールの作成に挑戦した話
Go初心者がGoでコマンドラインツールの作成に挑戦した話
 
Lockfree Queue
Lockfree QueueLockfree Queue
Lockfree Queue
 
SAT/SMTソルバの仕組み
SAT/SMTソルバの仕組みSAT/SMTソルバの仕組み
SAT/SMTソルバの仕組み
 
C++ マルチスレッドプログラミング
C++ マルチスレッドプログラミングC++ マルチスレッドプログラミング
C++ マルチスレッドプログラミング
 
Everyday Life with clojure.spec
Everyday Life with clojure.specEveryday Life with clojure.spec
Everyday Life with clojure.spec
 
いまさら恥ずかしくてAsyncをawaitした
いまさら恥ずかしくてAsyncをawaitしたいまさら恥ずかしくてAsyncをawaitした
いまさら恥ずかしくてAsyncをawaitした
 

ふつうのcore.async