ゲームプログラミングTips(1):クリティカルセクション
2012年3月7日……前回の講座より、なんと約5年も経過していた!
まあそんなどうでもいいことはさておき、久しぶりにはじまりはじまり〜。
1.クリティカルセクションとは?
ggrks
……と言ってしまったら一言で終了してしまうんで、一応説明(´Д`)
クリティカルセクションとは、「マルチスレッド動作時の排他制御」のことである。
詳しいことはWikipediaに載っているので、そちらを参照するがよろし<結局丸投げかよ!
2.クリティカルセクションのゲームプログラミングにおける活用事例
本コンテンツはゲームプログラミングTipsであり、つまりは活用事例を紹介しなければならない。
クリティカルセクションは、単一リソースに対して書き込みと読み込みを頻繁に行う場合に活用される。
「そんな場合ってあるの?」と思う貴方へ、実はあるのだ……ずばり、サウンドのストリーミング再生である。
サウンドのストリーミング再生は、以下の二つの動作を行っている。
1.ストリーミングバッファ(再生するサウンドの一部を収めたメモリのこと)を再生(=メモリの読み込み)
2.ストリーミングバッファにもうすぐ再生する箇所のサウンドの一部をファイルから読み込む(=メモリの書き込み)
上記の二つの動作を同一のメモリブロックに対して行うわけで、書き込み中は読み込ませたくないし読み込み中は書き込ませたくない。
そこでクリティカルセクションをメモリの書き込み箇所と読み込み箇所に使用し、同時に両方を行わないように排他制御を行うのである。
3.具体例(ヘッダ解説)
本項はクリティカルセクションに対する説明を行うため、サウンドのストリーミング再生のソースコードは掲載しない。
(というか、サウンドのストリーミング再生の説明は非常に長くなるので……何れ扱いたいとは思ってるけど)
本項では、俺が作ったクリティカルセクションクラスを掲載する。
/************************************************* ファイル名:CriticalSection.h 作成者 :あびす 役割 :クリティカルセクション *************************************************/ /** * @file CriticalSection.h * @brief 排他制御を行うクリティカルセクションクラスです。 |
ヘッダコメントやDoxygen対応コメントや二重インクルード防止や名前空間に対する説明は割愛、
ヘッダに対する解説では本クラスの使用方法を説明する。使用方法は簡単である、
1.インスタンスを生成する
具体例 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){ CCriticalSection CS; } |
……これだけである(´Д`)
インスタンスを生成することでクリティカルセクションが開始され、インスタンスの解体時にクリティカルセクションが終了する。
クリティカルセクションの実装は必ずしもこういった方式ではなく、開始関数・終了関数として実装する方式もあるが上記の方法にはふたつのメリットがある。
1.クリティカルセクションの終了を忘れること自体がありえない
開始関数・終了関数として実装した場合、終了関数の呼び出しを忘れると悲惨なことになる。
一方、インスタンスの生成・解体を開始・終了に対応させた場合、スコープから出る際に自動的に終了するようになる。
2.クリティカルセクションの開始・終了がソースコード上で分かりやすくなる
たとえば関数の頭でインスタンスを生成した場合、「この関数全体がクリティカルセクション適用範囲なんだな」と直感的に理解できる。
また、関数内の一部をクリティカルセクション適用範囲にしたい場合も、中括弧({})でローカルスコープを設ける必要があるため非常に分かりやすい。
他方、うっかりクリティカルセクション適用範囲を広くしすぎることはありえるが……それってこの方式特有のデメリットではないよね、ということで(´Д`)
4.具体例(ソース解説)
次はソース解説である。
#include "stdafx.h" #include "CriticalSection.h" namespace nsDX9A{ namespace nsMisc{ namespace nsCriticalSection{ //クリティカルセクション構造体保持クラス static class CCriticalSectionHolder{ public: //デフォルトコンストラクタ CCriticalSectionHolder(){ InitializeCriticalSection(&m_CS); } //デストラクタ ~CCriticalSectionHolder(){ DeleteCriticalSection(&m_CS); } //クリティカルセクション構造体 CRITICAL_SECTION m_CS; private: //コピーコンストラクタ(禁止) CCriticalSectionHolder(const CCriticalSectionHolder&); //代入演算子(禁止) CCriticalSectionHolder& operator =(const CCriticalSectionHolder&); } s_CCSH; //デフォルトコンストラクタ CCriticalSection::CCriticalSection(){ EnterCriticalSection(&s_CCSH.m_CS); } //デストラクタ CCriticalSection::~CCriticalSection(){ LeaveCriticalSection(&s_CCSH.m_CS); } } } } |
注目すべきところは、8〜26行目のCCriticalSectionHolderクラスである(というか、そこ以外の解説は割愛する)
このクラスはヘッダを見てもらえば一目瞭然だが、CCriticalSectionクラス使用者から見て「隠れている」クラスである。
このクラスの解説をする前に、WindowsAPIでのクリティカルセクションの使用方法について解説する。
使用方法は簡単で、
1.InitializeCriticalSectionでクリティカルセクション構造体(CRITICAL_SECTION)を初期化する
2.EnterCriticalSectionでクリティカルセクションを開始する
3.LeaveCriticalSectionでクリティカルセクションを終了する
4.DeleteCriticalSectionでクリティカルセクション構造体(CRITICAL_SECTION)を後始末する
これだけである。
ただし問題がひとつだけある、「どのタイミングでInitializeCriticalSectionとDeleteCriticalSectionを呼ぶのか?」だ。
無論、使う前と使った後に呼べばいい。……だが、もし呼ぶのを忘れてしまったら?
その対策として、俺は呼ばずとも初期化と後始末を自動で行ってくれるようにCCriticalSectionHolderという隠しクラスを用意したのである。
CCriticalSectionHolderクラスは静的変数であるため、プログラム開始時にコンストラクタが呼び出されその中でInitializeCriticalSectionを呼び出し、
プログラム終了時にデストラクタが呼び出されその中でDeleteCriticalSectionを呼び出すことが確実に保証される。
ただし良いことずくめではない、この方法にも問題はある。
静的変数の初期化順序は不定であるため、静的変数内でクリティカルセクションを必要とした場合に上手くいかない場合がある(上手くいく場合もある)
……まあ、クリティカルセクションを必要とするクラスを静的変数にしなければいいだけの話ではある。
そもそも、静的変数の初期化に依存すること自体が問題なのだ<その仕組みを使っているお前が言うな
まあ、どうしてもと言うのならこのクラスを介さずにクリティカルセクションを使ってもいいのだし。
……以上で本項は終了である、お疲れ様でした(´Д`)
いやぁ、適当に十分ぐらいで書こうと思ったら二時間も掛かってしまった(;´Д`)
いやはや、クラス設計の意図について語ると何だかんだで意外と時間が経ってしまうものである。
「ゲームのゲの字もないよ!」と思う方、マジすんません(;´Д`) 何時かは出せたらな〜、と思ったり思わなかったり。