C#非同期処理関連のMSDNの資料読んでみた(2)

今回はここを読んでいきます。

なんかMSDNの機械翻訳された資料に、ちょいちょい違和感を感じてて、
原文みたんですが、貼ったリンクに頻繁に登場するマネージスレッド、というワードですが、
Maneged threadということで、プログラマが生成し、管理するべきスレッドのことを指している
っぽいです。マネージスレッドという単語があまりにも登場するので、そういう固有名詞かと
勘違いしていましたが、まぁ普通にプログラマがプログラマを記述することによって
生成するスレッドのこと、だと思っても大丈夫だと思います。
(不都合がでれば、加筆修正していきます。)

ここでは、マルチスレッドを用いたプログラムを作成したときに、同期処理をサポートしてくれる
様々なクラスを紹介していきます。

前提知識

フォアグランドスレッドとバックグラウンドスレッド

違いは1点だけ

バックグラウンドスレッドはマネージ実行環境を実行されたままにしない

とMSDNにはありますが、あまりピンとこない。
探してみたらとてもわかりやすい記事がありました。

CodeZine .NETマルチスレッドプログラミング 1:スレッドの実行と同期

プロセス内のすべてのフォアグラウンドスレッドが終了した時に、
そのプロセスは終了し、同時にすべてのバックグランドスレッドは
強制的に終了させられます。逆に言えば、すべてのフォアグラウンドスレッドが
終了しなければプロセスは終了しませんが、バックグランドスレッドが
終了しなくてもプロセスは終了します

ということです。とてもよくわかりました

スレッド処理オブジェクトと機能


このページでは.NET Frameworkで利用できるスレッド処理に関連するオブジェクトや機能を
総括的に紹介しています。

ThreadPoolの利用

ThreadPoolクラスを用いて非同期処理を行うと、開発者はスレッド管理を気にすることなく、
アプリケーションのロジックに集中できるようになる。

.NET Framework4.0以降推奨されているTaskオブジェクトによる非同期処理も、内部的に
ThreadPoolを利用しています。

ThreadPoolを使用しないケース(そのまま引用)
  1. フォアグラウンド スレッドが必要な場合。
  2. スレッドに特定の優先順位を設定する必要がある場合。
  3. スレッドを長時間にわたってブロックするタスクがある場合。 スレッド プールにはスレッドの最大数が存在するため、多数のスレッド プール スレッドがブロックされると、タスクを開始できなくなることがあります。
  4. スレッドをシングルスレッド アパートメントに配置する必要がある場合。 ThreadPool スレッドはすべてマルチスレッド アパートメントにあります。
  5. スレッドに関連付けられた、確立された ID が必要な場合、またはスレッドを特定のタスク専用にする必要がある場合。
ThreadPoolの特徴
  1. ThreadPoolで利用されるスレッドは全てバックグラウンドスレッド
  2. 各スレッドは規定のスタックサイズを使用して、規定の優先順位で実行される(開発者が気にする必要なく使える)
  3. ThreadPoolは各プロセスに1つ存在する
利用例


Timerクラス

指定した時刻にデリゲートを呼び出すオブジェクト。スレッドプール内のスレッドで実行される。

TimerCallbackデリゲートにコールバックメソッドを渡し、時間を指定するだけで利用できる。
保留中のタイマーをキャンセル・破棄するにはTimer.Dispose()を呼び出す。

ここでとりあげるのはSystem.Threading.Timerで、System.Windows.Forms.Timerとは別です。
後者はwindowsフォームアプリケーションなど、ビジュアルデザイナーを操作するコントロールで、
ユーザインターフェースで利用されるために設計されています。そのためSystem.Windows.Forms.Timer
はユーザインターフェースのスレッドでイベントを発生させます。

対して、System.Threading.TimerやSystem.Timers.TimerはThreadPoolスレッドでイベントが発生します。

そして、System.Threading.Timerのみ、状態オブジェクトをコールバックメソッドに渡しますが、
他のタイマーは状態オブジェクトを提供しません。

使い方自体はだいたい同じですが、このようなちょっとした違いがあるので注意です。

利用例

実行すると、Console.ReadLine()によってユーザの入力を待った状態のまま、
TimerMethodがThreadPoolのスレッドによって実行され続けるので、.(ドット)が
1000ミリ秒間隔で実行され続けます。


WaitHandleクラス

Win32同期ハンドルをカプセル化し、複数の待機操作を実行するためのクラス。

WaitHandleクラス自体は抽象クラスで、実際には派生クラスと、イベント待機を
有効にする静的メソッドを利用します。

派生クラスにはMutex,EventWaitHandle,AutoResetEvent,ManualResetEvent,Semaphoreなどが
あります(後述)


EventWaitHandle、AutoResetEvent、CountdownEvent、ManualResetEvent

イベント待機ハンドルにより、スレッドは相互に通知を行い、相手の通知を待機して
動作を同期できます。イベント待機ハンドルは通知されたときに自動的にリセットされる
イベントと、手動でリセットする2種類に分けられます。

EventWaitHandle

自動または手動のリセットイベントを表す。

ローカルのイベント待機ハンドルだけでなく名前付システムイベント待機ハンドルを
表すことができる。(派生であるAutoResetEventやManualResetEventはローカルイベントのみ)

これは、MSDNのサンプルがとても秀逸でした。納得。

ここで登場するInterlockedは『非同期処理でも衝突なくインクリメントやデクリメントしてくれるクラス』
と考えてくれればOKです。

僕が無知なだけですが、AutoとManualって、なにが違うの?っていう疑問が最初、ありました。
(なにがAutoで、なにがManualなのか)

その疑問に関してはこちらのリンクが大変
参考になりました。

AutoResetEventは、待機中のスレッドがなくなると、自動的に非シグナル状態へと遷移します。

同じ処理をManualResetEventとAutoResetEventで比較しているサンプルは
こちら

AutoResetEventではシグナルを待機中のスレッドがすべて解放されると、自動的に非シグナル状態にリセットされるのです(待機中のスレッドがない場合は、無限にシグナル状態のままとなります)。

ManualResetEventはReset()を呼び出し、手動で非シグナル状態に戻す必要があります。

AutoResetEvent

EventWaitHandleの派生クラスで、自動的にリセットされるローカルイベントを表す。

※解説はEventWaitHandleのソースコード内で似たような使い方をしているため省きます。

ManualResetEvent

EventWaitHandleの派生クラスで、手動でリセットする必要があるローカルイベントを
表す。同じプロセス内のイベントに使用できる更に軽量で高速なManualResetEventSlim
クラスもあります。

※解説はEventWaitHandleのソースコード内で似たような使い方をしているため省きます。

CountdownEvent

CountdownEventは一定回数シグナル状態になった後、ブロック状態になる同期プリミティブ。
コンストラクタでシグナル状態になる回数を指定します。

サンプルはこちら

こんな感じ。使い勝手良さそう。


ミューテックス(Mutex)

僕の言葉でちゃんと説明する自信がないのですが、wikipediaにわかりやすく、かつ詳細に書かれていました。

ミューテックス

要するに、クリティカルセクション(プログラマが指定したとある処理)に1つしかアクセスできないようにする機能を
提供してくれます。

サンプルコードも短く、シンプルですぐに使えると思います。

サンプル


Interlockedクラス

こちらは先ほど説明した通り、複数スレッドからアクセスされる変数の操作を提供してくれます。
このクラスが提供するメソッドは例外をスローしない、ということもポイントですね。

サンプルコード

Interlocked.Exchange(ref int, int)は、第一引数にある参照へ、第二引数の値を代入し、第一引数にもともと
入っていた値を返すメソッドです。調べないとピンとこなかったので、念のため書いておきます。

プログラムを実行すると、だー、っと実行結果が出力されて、ちゃんと排他制御が行われているのか
確認するのが難しいですが、ゆっくり追っていけば確認できるかと思います。


読み取り / 書き込みロック

とあるリソースに対する排他的な読み取り/書き込みロックを提供する、というお話。

ReaderWriterLockとReaderWriterLockSlimの二種類のクラスがあるが、現在は全ての場合において
ReaderWriterLockSlimが推奨されています。(処理の簡素化、パフォーマンスの向上がなされているらしい)

というわけで、ここでもReaderWriterLockSlimを紹介します。
lockキーワードとは別なのでご注意ください。
サンプルコード

これもわかりやすく、非常に有用なサンプルだと感じました。
このクラスのインスタンス作成し、非同期でAddやDeleteしまくれば
排他制御ができていることを確認できると思います。

innerCacheに対する読み取り/書き込みアクセスがそれぞれ制御されていると思います。
(実質そうなっているというだけで、制御されているのはスレッドからのアクセスですが)

同期したい変数やメソッドへのアクセスは、このようにちゃんと制御してあげないとダメですね。

どこで読み取りロック(もしくは書き込みロック)を利用するべきか、というのが
本能的にわかっていないと、変なロックしちゃいそうですね。簡単に利用できる分、実装には注意が必要そうです。


同期プリミティブの概要

個人的にはこれを一番最初にもってきてほしかったんだが・・・w
一応、記事の通りの順番で確認していきました。

内容は案の定、既に紹介してきたものが多く含まれていたので、上記にないクラスや機能に関してだけ
かいつまんで挙げていきます。

マルチスレッドにおける同期を提供してくれる同期プリミティブは、大きく3つに分類できます。

  1. ロック
  2. シグナル
  3. インタロックされた操作
1.ロック

1度に1つのスレッド、または指定した数のスレッドがリソースを制御するようにする。

  1. lockステートメント(MonitorクラスのEnterとExitメソッドを利用)
  2. Mutex
  3. SpinLock
  4. ReaderWriterLock
  5. Semaphore
2.シグナル

別のスレッドからシグナルを待機する

  1. WaitHandle
  2. EventWaitHandle
  3. Mutex & Semaphore
  4. Barrier
3.インタロックされた操作

Interlockedクラスの静的メソッドで実行される分割不可能操作

インクリメント、デクリメント、加算、交換、比較などが利用できる。

  1. Interlocked

もうちょっと詳しく書いてありましたが、今回はざっくり。
同期プリミティブに関してしっかりカテゴリ分けして確認したい方はMSDNを参照してみてください。


Barrierオブジェクト

複数スレッドが段階的に1つのアルゴリズムで同時に動作できるようにする時プリミティブ。

これ、サンプルコード読んだだけだと、ピンと来なくて時間かかっちゃいました。

ひとまずサンプルコード

ポイントはBarrierクラスのコンストラクタでnew Barrier(int participantCount, Action postPahseAction)の
部分です。MSDN Barrierクラス

participantCountは、参加するスレッド数、postPhaseActionは参加するスレッド全てが、バリアポイントに
到達したときに実行する処理です。

サンプルで説明すると、t1とt2がそれぞれword1とword2をシャッフルして、最終的にbarrier.SignalAndWait()を
実行します。

t1とt2共に、SignalAndWait()に到達すると、barrier宣言時に定義したActionである
postPhaseActionが実行されます。

postPhaseActionが最後まで実行されると、Barrierインスタンスは解放され、t1とt2各スレッドで実行されている
Solve()内のwhileループが、再始動します。

これが繰り返されることによって、

  1. スレッドt1がword1をシャッフルしてbarrierをロック状態に
  2. スレッドt2がword2をシャッフルしてbarrierをロック状態に
  3. t1とt2の2つのスレッドからロック状態になったときにbarrierインスタンス定義時のpostPhaseActionが実行される
  4. postPhaseAction内でシャッフルされたword1とword2をつなげる
  5. 正解と等しいかチェック、正解なら終了。不正解ならロック解除する
  6. 1に戻る

こう動作しています。
同じアルゴリズムを複数スレッドからアクセスし、制御していることがわかります。


10. SpinLock構造体

ロックが使用可能になるまで、ロックを取得しようとするスレッドが
ループの繰り返しチェック内で待機する相互排他ロックの構造を提供します。

大量のブロックが予想される場合は、スピンロックの使用しないほうがいい、と
MSDNのドキュメントにはあります。ロックが細かくなったり、ロック保持時間が短い
時はスピンロックが有用です。

スピンロックを保持する際に避けるべきこと

  1. ブロッキング
  2. それ自体がブロックすることがある何かの呼び出し
  3. 一度に複数のスピンロック保持
  4. 動的にディスパッチされる呼び出しを行う
  5. 所有しないコードへの静的にディスパッチされる呼び出しを行う
  6. メモリ割り当て

うん、これだけ言われてもさっぱりですね。

MSDNでSpinLock構造体のドキュメントを読むより、まず『スピンロックとはなんぞ』
ということを確認すると上記の理由なんかがピンとくると思います。

スピンロック wikipedia

スピンロックそのものが説明された後、だいたいMSDNと同じような
懸念点が上げられていますね。まぁ短時間のブロックが頻繁に発生するような
構造なら、これ使うといいよ、程度です。

サンプルコード一応載せますが、記述自体は他のロックとそこまで代わりません。
他のロック構造と内部的に違う、というだけですね。


11. CancellationToken

CancellationTokenを用いるとThread, ThreadPool、そしてTaskオブジェクト間の連携の
取り消しが実現できます。

以下がサンプルコード。Task.StartNew()の第二引数でCancellationTokenを
受け取ることができます。あとは、実行コード内でCancellationTokenの
isCancellationRequestをチェックして動作させています。

Monitorクラスとlockステートメント

こちらはリンク内に紹介はないのですが、比較的良くつかわれる(らしい)
ので、まとめてこの記事で紹介したいと思います。

このサンプルコードではAutoResetEventやInterlockedなんかは
特別無視しちゃってもOK(なにをするためのクラスか、くらいは知ってたほうがいいけど)

lockステートメントを利用したクラス、Monitorクラスを利用したクラス、
そして最後にlockを利用しないで処理させるクラスの三種類のインスタンスを生成して、
マルチスレッド環境で処理を実行させています。

lockステートメントとMonitorクラスは、前述の通りMonitor.Enter()とMonitor.Exit()
をlockステートメントが内部的に処理しているだけ、というのが実感できると思います。

対して、同期プリミティブを一切利用しないUnSyncResourceクラスは
処理がスタートするのも、終了するのも順番がランダムになっているのが、
実行してみるとわかると思います。

さらっと同期させたいなら、結局これが楽チンですね。

蛇足だけど、この記事書いて、いくつかのサンプルコードを自分の手で書いて
うごかしていたら、WaitHandle関連にずいぶん慣れてきた気がします。

MSDNのサンプルコードを動かしているだけですが、コピペでなく、ソースコードを
理解しながら写経する、ってのいうのは、何も理解していない分野に関しては
やはり勉強になりますね。

おわり

いくつか省略したモジュールもあるのですが、主だった部分はひと通り
列挙しました。ただ、これ書き終わって読み終わると、本当にメモ書きで
全然参考になる気がしません。

書いた僕は勉強になりましたが、見てる人は直接MSNDのリンクを改めて
お読みになることをおすすめします。

とりあえず同期に利用できるクラス・構造体はこんなものがあるよ、ってことが
わかってもらえれば、と思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です