Upload
atsushi-kambara
View
666
Download
2
Embed Size (px)
DESCRIPTION
Reconsidering Multithreading Design Patterns
Citation preview
ReconsideringMultithreadingDesign Patterns
@atsukanrockAug. 2, 2014
めとべや東京 #5
@atsukanrock
Loves:• C#• ASP.NET• Azure•DDD (Domain-Driven Design)
http://atsukanrock.hatenablog.com/https://github.com/atsukanrock/
@atsukanrock
Loves:• C#• ASP.NET• Azure•DDD (Domain-Driven Design)
http://atsukanrock.hatenablog.com/https://github.com/atsukanrock/
http://jigokuno.com/?eid=162
というわけで•文字ばっかですごめんなさい• 絵を描く時間がなかった。。。
•一緒にコード見ましょう
このセッションのゴール
この辺を伝えたい•NET における並列プログラミング•スレッドの排他制御•スレッドの協調•デザインパターン一覧• Producer-Consumer pattern•そして Cloud へ
.NET における並列プログラミン
グ
ggrks 案件• http://ufcpp.net/study/csharp/lib_parallel.html
• http://www.atmarkit.co.jp/ait/subtop/features/dotnet/app/masterasync_index.html
• http://ufcpp.wordpress.com/2012/11/12/asyncawait%E3%81%A8%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E5%88%B6%E5%BE%A1/
ググったらすばらしい記事がたくさんあります。
進化してきた• Thread• ThreadPool• Asynchronous Programming Model
(APM)• Begin/EndInvoke
• Event-based Asynchronous Pattern• BackgroundWorker
• Task-based Asynchronous Pattern• Reactive Extensions (Rx)• async/await
async/await 様最高•書き方が超シンプルになる• シングルスレッドに見えるのに非同期にできる
• I/O 待ちの一本道処理はこれで勝つる• ネットワーク• ファイル• データベース ( ネットワーク + ファイル )
•→Web 系のほとんどをカバー
async/await 様最高•… んだけれども並列処理は不得手•書くとしたら :• Parallel.ForEach ?• Task.Run からの Task.WhenAll ?• Select(async el => …) からの Task.WhenAll ?
• “Select は非同期時代の ForEach”• http://neue.cc/2013/12/04_435.html
•とりあえず書けるけど…• デッドロック怖い• スレッド数が増えすぎて OutOfMemory になった
らどうしよう
async/await 様にも弱点が…•並列処理は async/await 様でも助けてくれない•とは言え今はマルチコア CPU の時代• CPU バウンドな処理は並列でやると早くなる、実際
正しい知識とデザインパターンで戦うのです !!
CPU バウンドな処理って?•画像処理•大量のテキスト処理•音声処理•ビデオ処理
というわけで•ここから、 CPU バウンドな処理に役立つ並列プログラミングを学びます
スレッドの排他制御
lock って書いたら•スレッド排他制御できる•↓ みたいな感じで lock の後の {} 内に入れるのは 1 スレッドだけ
lock (_lockObj){ _queue.Enqueue(item); Monitor.PulseAll(_lockObj);}
lock イメージ図•スレッド 1 と 2 が _lockObj のロックを取ろうとしている
_lockObj
スレッド 1
スレッド 2
メソッド 1
メソッド 2
lock イメージ図•スレッド 2 がロックを取った•スレッド 1 はロックが取れなかったので待たされている
_lockObj
スレッド 1
スレッド 2
メソッド 1
メソッド 2
lock イメージ図•スレッド 2 が lock の後ろの {} を抜けた•スレッド 1 にチャンスが回ってきた!
Lock Object
スレッド 1
スレッド 2
メソッド 1
メソッド 2
lock イメージ図•スレッド 1 がロックを取った
Lock Object
メソッド 1
メソッド 2
lock イメージ図•スレッド 1 も lock{} 抜けた
Lock Object
メソッド 1
メソッド 2
スレッドの協調
Wait / Pulse(All)•Monitor.Wait メソッドで、 lock{} の中で他のスレッドにロックを譲る•↓ みたいな感じlock (_lockObj){ while (_queue.Count >= _capacity) { Monitor.Wait(_lockObj); } _queue.Enqueue(item); Monitor.PulseAll(_lockObj);}
Wait / Pulse(All)•Monitor.Pulse(All) メソッドで、 Wait中の他のスレッドを起こして、ロック取得競争に仲間入りさせる•実は先スライドのコードにありました↓lock (_lockObj){ while (_queue.Count >= _capacity) { Monitor.Wait(_lockObj); } _queue.Enqueue(item); Monitor.PulseAll(_lockObj);}
Wait / Pulse(All)•スレッド 1 と 2 が _lockObj のロックを取り合って
_lockObj
スレッド 1
スレッド 2
メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
Wait / Pulse(All)•スレッド 2 がロック取得後、 Wait に到達した•スレッド 1 は待たされている_lockObj
スレッド 1
スレッド 2
メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
Wait / Pulse(All)•スレッド 2 が _lockObj の Wait セットに入ると、他スレッドにロック取得のチャンスが回ってくる
_lockObj
スレッド 1
スレッド 2
メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド2
Wait / Pulse(All)•スレッド 1 がロックを取得した•スレッド 2 は Wait セットで待っている
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド2
Wait / Pulse(All)•スレッド 1 が Pulse に到達した•スレッド 2 は Wait セットで待っている
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド2
Wait / Pulse(All)•Wait セットのスレッド 2 に起きろPulse が届く
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド2
Wait / Pulse(All)•スレッド 2 がロック取得待ちに戻る•ロックはまだスレッド 1 が持っている
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド 2
Wait / Pulse(All)•スレッド 1 が lock{} を抜けた• _lockObj のロックが取得可能になった
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド 2
Wait / Pulse(All)•スレッド 2 がロックを再取得した•スレッド 2 の処理は Wait の後から再開される _lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド 2
Wait / Pulse(All)•スレッド 2 が lock{} を抜けた• _lockObj のロックは解放される
_lockObj
スレッド 1メソッド 1
Monitor.Pulse(_lockObj)
メソッド 2
Monitor.Wait(_lockObj)
_lockObj の Wait セット
スレッド 2
Pulse と PulseAll の違い• Pulse は Wait セットにいる n 個のスレッドのうち 1 つしか起こさない• PulseAll は Wait セット内の全てのスレッドを起こす
• Pulse の方が高速と言われるが、 Pulse回数が足りていないと寝っぱなしになるリスクが
•個人的には常に PulseAll 推奨
一応他にも•↓ みたいなんあるけど• Manual/AutoResetEvent• Mutex
•Wait / Pulse(All) を上手く使えば大体の協調は実現できる• Manual/AutoResetEvent は
• 重たい(らしい)• 1 プロセス内で取れる数に上限がある(あった。今は不
明)• 使いまくったら突然死
• Mutex はそもそもプロセスまたいだ協調に使うもの• 他プロセスから見えるものが軽いわけない
デザインパターン一覧
何かいろいろある• Single Threaded Execution• Immutable•Guarded Suspension• Balking
何かいろいろある• Producer-Consumer• Read-Write Lock• Thread-Per-Message•Worker Thread
何かいろいろある• Future• Two-Phase Termination• Thread-Specific Storage• Active Object
説明しきれんけど•大体のパターンは普段使い or 簡単に理解できるので、個人的に最も重要かつ美しいと感じている Producer-Consumerだけに絞って解説
説明しきれんけど• Single Threaded Execution• 単に lock{} のこと
• Immutable• オブジェクトの状態がコンストラクタの後では変
えられないなら、マルチスレッドで排他制御 / 協調なしで扱っても安全• お手本 : System.String クラス
•Guarded Suspension• Producer-Consumer の特殊系とみなせる
• Balking• Producer-Consumer の特殊系とみなせる
説明しきれんけど• Producer-Consumer• この後どっぷり説明
• Read-Write Lock• ReaderWriterLock クラスってのが .NET 1.0 の頃
から(!) BCL にある。要は DB と同じ
• Thread-Per-Message• 普通そう組むやろ
•Worker ThreadThreadPool のことと捉えて問題ない
説明しきれんけど• Future• IAsyncResult とか Task ( Awaiter )みたいなも
ん。 JavaScript で言うと promiss
• Two-Phase Termination• CancellationToken での Cancel 的な。 Cancel
要求は無理やり殺さずフラグ立てるだけ。スレッド側がフラグチェックして安全に終了する
• Thread-Specific Storage• .NET 4.0 から BCL に ThreadLocal<T> クラスが
• Active Object• いろんなパターンの組み合わせらしい
Producer-Consumer pattern
3種類のオブジェクト• Producer• Consumer• Channel
Producer• Consumer への処理の Request を出す•例えるならわんこそば大会の調理係• 3 人で調理する• 競技者( Consumer )の目の前( Channel )が
一杯だったらちょっと待つ• 競技者の目の前が空いたら、そば( Request )を置いて調理場に戻り、次のそばを作り始める
Consumer• Producer からの Request を処理する•例えるならわんこそば大会の競技者• 5 人で争う• 目の前( Channel )に置かれたそば
( Request )があればすぐに食べ始める• 目の前にそばが置かれていなければ、そばが置か
れるまで待つ(大問題 !! )
Channel• Producer が Request を置く場所• Consumer が Request を取る場所•プログラム的には、 Producer やConsumer を待たせるのが最重要な役割
Channel のサンプル• https://github.com/atsukanrock/MultithreadDesignPattern/blob/master/MultithreadDesignPattern/ProducerConsumer/Channel.cs•ポイント :• Add メソッドで容量( _capacity )いっぱいの場
合は Producer スレッドを待たせている( Monitor.Wait )• Take メソッドで空の場合は Consumer スレッド
を待たせている( Monitor.Wait )• Request 数の変化時に待っているスレッドを起こ
している( Monitor.PulseAll )
Producer のサンプル• https://github.com/atsukanrock/MultithreadDesignPattern/blob/master/ProducerConsumer.ConsoleApp/Producer.cs• とその基底クラスたち
• 抽象化したのでちょっと分かりづらいです。。。
•ポイント :• スレッドを意識したコードがない
Consumer のサンプル• https://github.com/atsukanrock/MultithreadDesignPattern/blob/master/ProducerConsumer.ConsoleApp/Consumer.cs• とその基底クラスたち
• 抽象化したので (ry
•ポイント :• スレッドを意識したコードがない !!
Producer-Consumer pattern の美しい所•スレッドの協調を意識しているのがChannel だけ• Producer と Consumer はフルパワーで働いている(つもりになっている)だけ• 作っていた / 食べていたと思っていたら待ってい
た !!
•複雑さの局所化•シンプルさを保てる• KISS ( Keep It Simple, Stupid )の原則、大事
です。
Producer-Consumer pattern の注意点• Producer の生産力と Consumer の消費力に差があり過ぎて、かつ Channel のCapacity が小さい場合、待ち状態のスレッドがたくさんになる
•NotifyAll でロックを取るスレッドが、Capacity 一杯 or 空状態を解消できないスレッドになる可能性が高くなる( NotifyAll のアルゴリズムがラウンドロビンなら良いんだけど、、、未調査)•NotifyAll の空打ちが頻発する
Producer-Consumer pattern の注意点
•NotifyAll の空打ちが頻発する
• Producer と Consumer のスレッド数決定を慎重にすること
前スライドからの矢印
Producer-Consumer pattern の発展形•典型的な(シンプルな) Channel はQueue ( FIFO )で実装される• Stack ( LIFO )にしても良いし•最後の Request だけ取って他のRequest は捨てるという形(“ LIFO-and-Clear” と名付けよう)もあり得る• 例えば Kinect プログラミングでは、センサーから
の入力情報が無慈悲に飛んでくる• ちょっと重たい画像処理をかける場合などには、
全部は処理しきれない基礎を理解していれば応用が効きます。
Producer-Consumer pattern の発展形•時間があったら LIFO-and-Clear をライブコーディング(無理やな)• LIFO-and-Clear程度なら Rx を上手く使えばデフォで書けるけんども• Rx は Producer-Consumer pattern の Channel
役を立派にこなしてくれる• ソースがマルチスレッドでもシリアライズしてく
れる(らしい)し• Sample メソッドで n ミリ秒に 1 回だけデータを拾ったり、 Buffer メソッドである程度溜めてから拾ったり
• Channel自体はシンプルなクラスで済むから、自分で書いてかゆい所に手を届かせるのもヨシ• かゆうま
Producer-Consumer pattern のデモ
画像処理の高速化•手元のスマホで↓にアクセス !!• http://imgproc.cloudapp.net/
• 1日限定です。課金怖い
画像処理の高速化•ソースは https://github.com/atsukanrock/MultithreadDesignPattern• ゴミ多いので注意されたし
• 当初、 Azure の Worker Role で処理させる目論見だった
• Emulator ではバッチリ動いた。ヒャッハー• Azure にデプローイ !!→動かぬ。ずっと Busy• 急遽方針変更。 WPF 上で全部やることに( 8/2 1:30
頃)• WPF にぎりぎり移しきった(イマココ)
画像処理の高速化•時間があれば、ソースの説明• SignalR
• Publish from ASP.NET Web API• WPF client
• BlockingCollection<T>• BCL に含まれている Channel クラス• コンストラクター引数で Queue / Stack の切り替えが
可能• CompleteAdding メソッドで Producer の完了を
Channel に通知できる• Consumer は IsCompleted プロパティを見て終了判定
• WorkerBase<T>• 典型的な Consumer の基底クラス• 処理結果はイベントで返す→ Rx で受けるのが ( ・∀・ )イイ !!
そして Cloud へ
Producer-Consumer pattern って…• Azure の Queue と Worker に似てる•その通り• Channel はインプロセスの Queue そのもの
Producer-Consumer pattern って…•Queue の場合は永続化されている
•対象外性に優れている• Producer や Consumer が急にお亡くなりになっても Queue は生きている。そう、 Queue は生きている
Producer-Consumer pattern って…•Queue の場合は永続化されている
• Channel自体のパフォーマンスはもちろんインプロセスの方が良いが、 Request の追加 / 取得自体より処理の方が重たいのが普通だから、問題にならない•インプロセスではスケールアウトできないからすぐ上限が来るし
Cloud 最高… ?!•データを Cloud に持っていくのでセキュリティがー•Worker をいっぱい立てたら課金がー
•エンタープライズでは意外と気軽に使えなかったりする•手軽にマルチスレッドデザインパターンを実装できると役に立つ
Cloud 最高… ?!• BIG DATA (笑)の処理などプロセス境界を容易に超えられるものは Cloud へ• 節子、それ発明やない。 MapReduce や
•センサーデータ処理などプロセス境界超えられないものはマルチスレッドデザインパターンで
まとめ
大事なこと• async/await だけではマルチスレッドの協調が必要な並列処理は書けない•Wait / Pulse / PulseAll でおk• Immutable厨にならないように気をつけよう• 僕は昔なりました
• Producer-Consumer は超応用が効く
•Worker Role 死すべし• 嘘です勉強します。。。
質疑応答