と言うわけでなぜか選んだのは「WinAPIの同期処理」という適当に検索すればいろいろな場所にその意味が書いてある物を扱ってみます。
一度下書きで書いたまま放置されていたので文面が微妙かもしれませんが・・・。
今回の場合は特に「使い勝手」と「同期の速度」に重点を置いてそれぞれを考えてみたいと思います。
そもそも、WinAPIで使用できる同期オブジェクトの種類は?
この場合の「同期」とは
スレッドなど並列処理中に意図しない値の変更などを避けるために待機処理などを行う処理および動作
と言う意味で使用する物とします。
そうすると、WinAPI中には大別すると以下の5つの同期オブジェクトが存在します。
- クリティカルセクション(CriticalSection)
- ミューテックス(Mutex)
- セマフォ(Semaphore)
- イベント(Event)
- インターロック(Interlocked)
このうち、ミューテックスのみは同期の適応範囲がOS全体に広がる物なのでその用途で使用するときのみ使用します。
そのため、今回の説明では除外することにします。ちなみにその性格上同期の速度は最低で、プロセス内での同期では基本的に使用されません。
また、これらのオブジェクトのうち、(速度を無視すれば)いくつかは別のオブジェクトを組み合わせることで再現できる物もあります。
たとえば、セマフォはイベント+インターロックで同じような動作を再現できますし、クリティカルセクションもセマフォで再現できます。
なので、必ずしもすべてを使用する必要はありませんし、マルチスレッドのライブラリでもこの5つをすべて備えていると言うパターンはあまりなく、
必要に応じてそれぞれを再現するようなことも必要になるかもしれません。
4つの同期処理を比較してみよう
と言うことで比較してみます。
クリティカルセクション(CriticalSection)
読んで字のごとく・・・とよく説明されている物です。
ある部分を保護し、保護された部分は必ず単一のスレッドだけが実行できる、と言う動作を再現します。
相互排他処理においては基本中の基本となる同期オブジェクトですね。
使用するときには
処理 | 対応関数(API) |
---|---|
初期化 | InitializeCriticalSection,InitializeCriticalSectionAndSpinCount |
排他待機 | EnterCriticalSection,TryEnterCriticalSection |
排他解放 | LeaveCriticalSection |
解放 | DeleteCriticalSection |
によって行います。
一応MSDNのヘルプ上では「クリティカルセクションの構造体の中身はブラックボックスとして扱う」と言うことになっていますが、実際にはヘッダを検索すれば定義は簡単に出てきます。
ほとんどの場合は「スピンカウント[1](短時間待機用)」+「セマフォ(長時間待機用)」と言う二つの同期オブジェクトによって構成されています。
クリティカルセクションの特徴は
- 完全な排他処理を行う(単一スレッドによる処理を強制する)
- ロック速度が速い(短時間の場合はスピンカウントを用いるため)
- 同一スレッドが複数回ロックすることが可能(その場合はロックした回数だけ解放が必要)
- タイムアウトと言う概念を持っている同期オブジェクトではないので一度待機状態に入ると「ほぼ無限に」待ち続ける
です。
セマフォ(Semaphore)
あまりゲーム等では使用されるオブジェクトではありませんが、マルチスレッド処理ではよく出てくる同期オブジェクトです。
「カウントを持っている同期オブジェクト」ですね。
使用するときには
処理 | 対応関数(API) |
---|---|
初期化 | CreateSemaphore |
排他待機 | WaitForSingleObject,WaitForSingleObjectEx,WaitForMultipleObjects,WaitForMultipleObjectsEx,・・・ |
排他解放 | ReleaseSemaphore |
解放 | CloseHandle |
によって行います。
ゲームで使用するとするならば主にスレッドプール[2]を管理するときに使用します。
マルチスレッド上のタスク管理で待機させてあるスレッドに動作開始を通知するときに使用することが多いですね。
描画分割時のスレッドの管理くらいですか。一般的にはもっといろいろと使用方法がありますが。
セマフォの特徴は
- WinAPI系では同期オブジェクトのベースになっている(場合が多い。クリティカルセクションの内部実装など)
- 初期化時に最大カウント数を設定することができ、カウント数の増減で同時実行可能スレッド数を制御する
- 主にタスク管理の同期として用いられる
です。
イベント(Event)
WinAPI系ではクリティカルセクションと並んで使用される同期オブジェクトです。
イベントの名の通り「通知」に重点を置いたものです。
使用するときには
処理 | 対応関数(API) |
---|---|
初期化 | CreateEvent |
排他待機 | WaitForSingleObject,WaitForSingleObjectEx,WaitForMultipleObjects,WaitForMultipleObjectsEx,・・・ |
排他解放 | SetEvent(シグナル状態に移行),ResetEvent(非シグナル状態に移行) |
解放 | CloseHandle |
によって行います。
特にWinAPIでのイベントはちょっと特殊で、ほかの様々なオブジェクト中でマルチスレッドによる実行通知を行う場合、だいたいイベントが中間で用いられます。(DirectSoundの再生位置通知など)
また、イベントを手動でシグナル状態[3]と非シグナル状態[4]とを切り替えるように設定することもできます。
イベントの特徴は
- WinAPIで処理の通知に用いられることが多い
- 待ち状態に関する設定を手動で行うことができる
です。
インターロック(Interlocked)
これだけちょっとほかの同期オブジェクトとは同期の考え方が異なります。
単純に「ある演算の間だけ処理を割り込まれないようにする」というものです。
対応している演算は本当に基本的な物だけで、
- 加算、減算
- インクリメント、ディクリメント
- AND、OR、XOR
- 値の交換(値の代入)
くらいです。
もちろん、クリティカルセクションを使っても同様なことはできますが、インターロックを使った演算の良いところはその速度です。
WinAPI系の場合(この場合はIntelArchitecture系CPU)であれば、これらはCPUの命令として実装されていますので、
単に演算を相互排他するだけならクリティカルセクションより速いです。
一応いろいろと考えてあったのだと思います。おそらくCPU上に実装されていないときのためにWinAPI扱いになっていて、
- CPUに命令が実装されている=>CPUの命令として実行
- CPUに命令が実装されていない=>システムクリティカルセクションによる保護を行い相互排他を行う
- 同時に実行可能なスレッドはシステム内では一つしか存在しない=>そのまま演算
と言う動作を目的としたものだったのでしょう。VisualStudio2005以上からは組み込み命令としてもあることからこのように考えられます。
同期オブジェクトの使い分け・・・
というわけで、それぞれの同期オブジェクトを見てきました。
ゲームプログラムでは基本的に使用するのは「クリティカルセクション」と「イベント」です。
セマフォを使用する機会というとせいぜい描画分割によるタスク管理やネットワークゲームを作るときの通信処理くらいです。
インターロックは使うとするならマルチスレッドで困る参照カウンタの処理が主ですか。
- 指定された回数だけ同期オブジェクトによって排他処理ができないかを確認するというもの。このカウントの間だけOSによる待ち状態をさけ、スレッドをアクティブにしたまま待機(空命令など)し、排他処理に移行できないかを確認する[back]
- あらかじめスレッドを作成しておいて、待機させておく領域[back]
- 通知が行われ、待機処理による待機が解除される状態[back]
- 待機処理による待機が実行される状態[back]