CreateMutexで安全な排他制御を実装する
Windows APIのCreateMutexは、複数のスレッドやプロセスが同じ共有資源へ同時にアクセスすることを防ぐための同期オブジェクトです。ファイル、設定情報、名前付きパイプ、単一起動アプリケーションの判定など、Windowsプログラミングで幅広く利用できます。
ミューテックスはカーネルオブジェクトとして管理されるため、同一プロセス内の排他制御だけでなく、名前を付けたプロセス間同期にも対応します。使い方の要点は、取得、処理、解放、ハンドル破棄というライフサイクルを崩さないことです。
| 手段 | 主な用途 | プロセス間で共有 | 所有権の概念 | 注意点 |
|---|---|---|---|---|
CreateMutex |
スレッド・プロセス間の排他 | 可能 | あり | 待機と解放が必要 |
CRITICAL_SECTION |
同一プロセス内の高速な排他 | 不可 | あり | 初期化と破棄が必要 |
SRWLOCK |
同一プロセス内の軽量な同期 | 不可 | あり | 再帰取得に注意 |
CreateEvent |
状態通知や処理完了通知 | 可能 | なし | 排他用途とは設計が異なる |
ミューテックスの基本的な動作
ミューテックスは、あるスレッドが所有している間、ほかの待機スレッドを停止させます。WaitForSingleObjectなどで取得を試み、処理が終わったらReleaseMutexで所有権を手放します。待機中のスレッドは、所有権が解放されるまで処理を続行できません。
CreateMutexのbInitialOwnerをTRUEにすると、呼び出し元スレッドが作成直後のミューテックスを所有します。ただし、既存の名前付きミューテックスを開いた場合も同じ値だけで判断すると誤りやすいため、戻り値とGetLastErrorを組み合わせて状態を確認します。
HANDLE mutex = CreateMutexW(
nullptr,
FALSE,
L"Local\\SampleApplicationMutex"
);
if (mutex == nullptr) {
// 作成失敗
return 1;
}
DWORD result = WaitForSingleObject(mutex, INFINITE);
if (result == WAIT_OBJECT_0) {
// 共有資源を使う処理
ReleaseMutex(mutex);
}
CloseHandle(mutex);
名前付きミューテックスで単一起動を制御する
名前を指定したミューテックスは、同じセッションやコンピューター上の別プロセスから参照できます。アプリケーションの二重起動を防ぐ場合は、起動時に固有名で作成し、GetLastError()がERROR_ALREADY_EXISTSを返したら、すでに別のインスタンスが動いていると判断します。
HANDLE mutex = CreateMutexW(
nullptr,
TRUE,
L"Local\\Company.Product.UniqueMutex"
);
if (mutex == nullptr) {
return 1;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
CloseHandle(mutex);
return 0;
}
グローバル名前空間を使用する場合は、Global\接頭辞を利用します。ただし、セッションをまたぐオブジェクトではアクセス権やサービス実行環境の違いが問題になることがあります。通常のデスクトップアプリケーションでは、必要性がなければLocal\を選ぶほうが扱いやすい設計です。
待機結果を正しく判定する
WaitForSingleObjectの結果は、単純な成功と失敗だけではありません。WAIT_OBJECT_0は所有権の取得、WAIT_TIMEOUTは指定時間内に取得できなかった状態、WAIT_ABANDONEDは前の所有者が解放せず終了した状態を示します。無限待機を避けたい処理では、適切なタイムアウト値を設定します。
DWORD result = WaitForSingleObject(mutex, 5000);
switch (result) {
case WAIT_OBJECT_0:
// 正常に所有権を取得
break;
case WAIT_ABANDONED:
// 前回の処理が途中で終了した可能性がある
break;
case WAIT_TIMEOUT:
// 取得できないため処理を中止または再試行
break;
default:
// APIエラー
break;
}
WAIT_ABANDONEDでも呼び出し元はミューテックスを取得した状態になります。しかし、保護対象のデータが中途半端な状態に残っている可能性があるため、そのまま正常データとして扱うのは危険です。ファイルの整合性確認や一時ファイルからの復旧など、異常終了を想定した処理を追加します。
所有権の解放とハンドル管理
ミューテックスを取得したスレッドだけがReleaseMutexを呼び出せます。取得していないスレッドから解放しようとすると失敗するため、取得結果を変数で保持し、成功した場合だけ解放する構造にします。例外や早期リターンが多いC++コードでは、RAIIラッパーを使うと解放漏れを抑えられます。
CloseHandleはカーネルオブジェクトへの参照を閉じる処理であり、ミューテックスの所有権を明示的に解放するものではありません。所有中にハンドルを閉じると、スレッド終了時の扱いが複雑になり、放棄されたミューテックスとして検出されることがあります。必ずReleaseMutexを先に実行し、その後でCloseHandleを呼び出します。
Windows APIの呼び出し順序やウィンドウメッセージ処理を整理する資料として、Windows APIの技術情報も参照できます。同期処理を画面アプリケーションへ組み込む場合は、待機中にUIスレッドを止めない設計が重要です。
排他範囲を小さく設計する
ミューテックスを保持する時間が長いほど、ほかのスレッドやプロセスの待機時間も増えます。ファイルを開く、ネットワークへ接続する、ユーザー入力を待つといった時間の読めない処理を、排他区間へ無制限に含めるのは避けます。共有データの読み書きに必要な範囲だけを保護するのが基本です。
排他区間の設計では、次の点を先に決めておくと実装が安定します。
- どのデータを共有資源として扱うか
- どの処理が所有権を取得するか
- 待機時間の上限を何秒にするか
- 取得失敗時に再試行するか中止するか
- 異常終了後のデータをどう検証するか
複数のミューテックスを使う場合は、取得順序を全スレッドで統一します。スレッドAがミューテックス1を取得してから2を待ち、スレッドBが2を取得してから1を待つと、相互待ちによるデッドロックが発生します。資源に番号を付け、常に小さい番号から取得する方法が分かりやすい対策です。
セキュリティ属性と名前の衝突
CreateMutexで指定した名前は、別のプログラムが予測して利用できる可能性があります。高い権限で動作するプロセスと一般ユーザーのプロセスが同じ名前を使うと、意図しないアクセスやサービス拒否につながる場合があります。必要に応じて長く固有性の高い名前を使い、セキュリティ記述子も検討します。
名前付きオブジェクトを作成するときは、SECURITY_ATTRIBUTESの設定、継承フラグ、Local\またはGlobal\の選択を環境に合わせます。管理者権限で動くサービス、複数ユーザーがログオンする端末、サンドボックス環境では、同じコードでも可視範囲やアクセス権が変わることがあります。
ウィンドウを操作するプログラムでは、同期処理とメッセージ処理を分離する必要があります。ウィンドウプロシージャの役割を整理する際は、ウィンドウプロシージャの実装のような関連資料も役立ちます。UIスレッドで無限待機を行わず、ワーカースレッドや非同期処理へ切り出します。
実装前に確認するポイント
排他制御はAPIを呼べば自動的に安全になる機能ではありません。共有資源を使うすべての経路が同じミューテックスを参照し、取得前にアクセスしないことが条件です。片方の処理だけがロックを使うと、保護範囲に穴が残ります。
テストでは、複数プロセスの同時起動、待機中の終了、タイムアウト、アクセス拒否、異常終了後の再起動を確認します。特にERROR_ALREADY_EXISTSとWAIT_ABANDONEDは通常実行だけでは現れにくいため、意図的に条件を作って検証します。
class MutexGuard {
public:
explicit MutexGuard(HANDLE handle) : handle_(handle), locked_(false) {
DWORD result = WaitForSingleObject(handle_, 5000);
locked_ = (result == WAIT_OBJECT_0 ||
result == WAIT_ABANDONED);
}
~MutexGuard() {
if (locked_) {
ReleaseMutex(handle_);
}
}
bool locked() const { return locked_; }
private:
HANDLE handle_;
bool locked_;
};
このようなガードを使う場合でも、WAIT_ABANDONEDを正常取得と同じに扱うかは、保護対象のデータ構造によって決めます。単なる単一起動判定なら影響は限定的ですが、書き込み途中のデータを保護する用途では復旧処理が必要です。
安全な運用につなげる設計
CreateMutexによる排他制御では、作成時のエラー、既存オブジェクトの判定、待機結果、所有権の解放、ハンドルの破棄を一続きの設計として扱います。単にCreateMutexを呼び出すだけではなく、誰が、いつ、どの資源を、どれだけの時間保護するかを明確にします。
実装時は、名前付きミューテックスの命名規則を定め、待機時間に上限を設け、取得できなかった場合の動作を決めます。共有データの整合性確認と異常終了からの復旧も含めて設計すれば、スレッド間同期からプロセス間同期まで安定して対応できます。最終的には、取得成功時だけ処理し、必ず解放してからハンドルを閉じる流れをコードの基本形にします。