CreateSemaphoreでWindowsのリソース数を安全に制限する
Windows APIで複数のスレッドやプロセスから共有リソースを利用する場合、同時実行数を適切に制御しなければなりません。データベース接続、ファイル処理、ソケット通信、ワーカースレッドなどには、利用可能な数に上限があります。CreateSemaphoreは、このような数量制限をカーネルオブジェクトとして管理するための代表的なAPIです。
セマフォは、内部にカウンターを持つ同期オブジェクトです。カウンターが1以上のときに待機関数が成功すると値が1減り、ReleaseSemaphoreを呼び出すと値が増えます。初期値と最大値を設定できるため、同時に使用できるリソース数をそのまま表現できます。
たとえば、同時に3本まで利用できる接続プールなら、最大カウントを3に設定します。4本目の利用者は空きが出るまで待機し、処理が終わったスレッドがセマフォを解放します。排他ロックのように常に1個だけを許可するのではなく、一定数まで並列実行を認められる点が特徴です。
Windows APIの宣言や関連する同期処理を確認するときは、古い環境向けの仕様も含めて整理されたWindows API資料を参照すると、関数の引数や戻り値を調べる手掛かりになります。実装では、ハンドルの所有、待機結果、エラーコードまで一体として扱うことが重要です。
| 項目 | CreateSemaphore | Mutex | Event |
|---|---|---|---|
| 主な用途 | 同時利用数の制限 | 単一所有者による排他 | 状態や通知の伝達 |
| 内部状態 | 0から最大値までのカウント | 所有スレッドを持つ | シグナル状態 |
| 待機成功時 | カウントが1減る | 所有権を取得 | 状態に応じて変化 |
| 解放方法 | ReleaseSemaphore | ReleaseMutex | SetEventまたはResetEvent |
| 複数プロセス | 名前付きで可能 | 名前付きで可能 | 名前付きで可能 |
セマフォのカウントと基本動作
CreateSemaphoreの代表的な宣言は、次のような形です。
HANDLE CreateSemaphoreW(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCWSTR lpName
);
lInitialCountは作成直後の利用可能数、lMaximumCountはカウンターの上限です。初期値は0以上、最大値以下でなければなりません。最大値を5にした場合、最大5個の待機要求までが即時に通過できます。初期値を0にすると、ReleaseSemaphoreが呼ばれるまで、すべての待機側が停止します。
利用側はWaitForSingleObjectやWaitForMultipleObjectsでセマフォのハンドルを待ちます。待機がWAIT_OBJECT_0になった場合だけ、リソースを取得したものとして処理を開始します。待機関数が成功した時点でカウントは減少しているため、処理終了時には必ずReleaseSemaphoreを呼び出してカウントを戻します。
HANDLE sem = CreateSemaphoreW(nullptr, 3, 3, nullptr);
if (sem != nullptr) {
DWORD result = WaitForSingleObject(sem, 5000);
if (result == WAIT_OBJECT_0) {
// 制限されたリソースを使用する
ReleaseSemaphore(sem, 1, nullptr);
}
CloseHandle(sem);
}
リソースプールに組み込む設計
接続やワーカーのプールでは、セマフォを「空き枠の数」として扱うと設計が明快になります。処理開始前に待機し、実際のオブジェクトをプールから取り出し、処理後にオブジェクトを返してセマフォを解放します。セマフォのカウントとプール内の実体数が一致していることが大切です。
例外や早期リターンがある場合、解放処理を必ず通過する構造にします。C++ならRAIIラッパー、Cなら後処理ラベル、C#などの相当環境ならfinallyに相当する仕組みを使うと、途中でエラーが発生しても枠が永久に失われる事態を防げます。
運用時に確認すべきポイントは次のとおりです。
- 待機前にリソースを取得しない
- 成功した待機に対して一度だけ解放する
- タイムアウト時は取得済みとして扱わない
- 最大カウントを超えてReleaseSemaphoreを呼ばない
- 失敗したハンドルを待機関数へ渡さない
ReleaseSemaphoreは、増加後のカウントが最大値を超える場合に失敗します。戻り値を確認せずに処理を続けると、実際のプール状態と同期オブジェクトの状態が食い違います。特に複数の終了経路があるコードでは、二重解放が発生しやすいため注意が必要です。
タイムアウトとエラー処理
無期限のINFINITE待機は、単純な処理では便利ですが、リソース障害や処理停止が起きた際にスレッドが戻らなくなる可能性があります。利用時間に上限がある処理では、ミリ秒単位のタイムアウトを設定し、WAIT_TIMEOUTを受け取った場合に適切な中断や再試行を行います。
WaitForSingleObjectの結果は、成功、タイムアウト、失敗を区別して処理します。WAIT_FAILEDの場合はGetLastErrorで原因を取得できます。ハンドルが閉じられた、無効な値が渡された、終了処理と待機処理が競合した、といった問題は、単なるタイムアウトとは異なるため、ログに残す情報も分けるべきです。
処理の流れを次のように分けると、リソースの所有状態を追跡しやすくなります。
- 待機を開始する
- WAIT_OBJECT_0だけを取得成功とする
- 成功後にプールの実体を確保する
- 実体の確保に失敗したら直ちに枠を戻す
- 使用完了後に一度だけReleaseSemaphoreを呼ぶ
- CloseHandleは待機中のスレッドがない段階で実行する
セマフォは、所有スレッドを記録するMutexとは異なります。あるスレッドが取得した枠を別のスレッドが解放することも可能ですが、設計上それを許すかは明確に決める必要があります。所有関係を厳密にしたい処理ではMutex、単純な数量制限ではSemaphoreという使い分けが基本です。
名前付きセマフォとプロセス間共有
lpNameに名前を指定すると、同じセッションや名前空間にある別プロセスから、同じ名前のセマフォを開けるようになります。CreateSemaphoreを同じ名前で呼び出した場合、既存オブジェクトのハンドルが返され、GetLastErrorはERROR_ALREADY_EXISTSになります。新規作成と既存利用を区別するには、この値を確認します。
名前付きオブジェクトでは、異なるアプリケーションが同じ名前を偶然使わないよう、製品名や用途を含む一意性の高い名前を設定します。サービスとデスクトップアプリケーションの間で共有する場合は、セッションやGlobal、Local名前空間、アクセス権の違いも確認が必要です。
セキュリティ属性をnullptrにした場合のアクセス条件や、ハンドル継承の設定も検討します。プロセス間通信で利用するなら、誰がオブジェクトを作成し、誰が開くのかを決めたうえで、必要最小限の権限だけを与える構成が安全です。
スレッド制御との違いと実装上の注意
スレッドを一時停止させるAPIとセマフォは、役割が大きく異なります。SuspendThreadは実行中のスレッドを強制的に止める操作であり、共有データを安全に保護したり、利用可能なリソース数を数えたりするものではありません。動作の違いはスレッド停止の解説でも確認できます。
処理の流量を制限したいときは、スレッドを外部から停止するのではなく、対象スレッド自身がセマフォを待つ設計にします。これにより、処理の開始点と終了点が明確になり、ロック保持中の停止やDLL内部での停止といった危険を避けやすくなります。
実装を安定させるには、セマフォを単独で置くのではなく、リソースの取得、使用、返却を一つの管理単位にまとめます。作成時には初期値と最大値を検証し、利用時には待機結果を分岐し、終了時にはすべての利用者が待機を終えた後でハンドルを閉じます。CreateSemaphoreは、正しいカウント管理と確実な解放を組み合わせることで、共有リソースの同時利用数を予測可能に制御できます。