Semaphore(セマフォ)は、カウンタと待機スレッドのキューを通じて共有リソースへのアクセスを制御する同期プリミティブです。Wikipedia, 2024によると、セマフォは1965年にEdsger Dijkstraによってマルチスレッド相互作用の問題を解決するために提案されました。このツールは、クリティカルセクションと同時に作業するスレッドの数を制限できます。
主要ポイント
Semaphoreは、カウンタを使用して共有リソースへのアクセスを制御する同期プリミティブです。この概念は1965年にEdsger Dijkstraによって提案され、オペレーティングシステムにおけるすべての現代的な同期メカニズムの基盤となりました。
セマフォは、wait(acquire)とsignal(release)という2つのアトミック操作を持つ整数変数です。wait操作はカウンタを減少させ、signal操作はカウンタを増加させます。カウンタがゼロに達すると、waitを呼び出したスレッドは、別のスレッドがsignalを実行するまでブロックされます。
セマフォの主な目的は、クリティカルセクションを複数のスレッドによる同時アクセスから保護することです。ミューテックスとは異なり、セマフォは所有者スレッドへのバインドを必要とせず、より広範な調整タスクに適しています。
セマフォの概念は、Technische Hogeschool Eindhovenで開発されたTHEオペレーティングシステムの文脈で生まれました。Dijkstraはセマフォを数学的抽象化として形式化し、任意の同期プリミティブを実装するための十分性を証明しました。
セマフォメカニズムは、2つのアトミック操作と内部待機キューに基づいています。acquireが呼び出されると、スレッドはカウンタ値をチェックし、実行を続行するか、リソースが解放されるまでブロックされます。
セマフォを作成する際、許可カウンタの初期値が設定されます。acquireを呼び出すたびにカウンタが1減少します。その後カウンタが負になると、スレッドはブロックされます。release操作はカウンタを増加させ、待機中のスレッドの一つを起こします。
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} は動作中")
} finally {
semaphore.release()
}
}
スレッドがゼロカウンタでacquireを呼び出すと、OSはそのスレッドをセマフォのFIFOキューに配置します。スレッドはBLOCKED状態に移行し、CPU時間を消費しません。release呼び出し後、キューの最初のスレッドがRUNNABLE状態に移行し、リソースへのアクセスを取得します。
同期理論では、セマフォの2つの主要なタイプが区別されます:バイナリとカウンティングです。タイプの選択は、リソースへのアクセス管理の特定のタスクに依存します。
バイナリセマフォは値0と1のみを取ります。動作的にはミューテックスに似ていますが、所有権要件がありません—任意のスレッドがreleaseを実行できます。このようなセマフォは、スレッド間の準備完了フラグやイベントの実装に便利です。
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("データ準備完了")
}
カウンティングセマフォは任意の非負の値を取ることができます。これは、複数のインスタンスが利用可能な同種リソースのプールを管理するために使用されます。例えば、5つのネットワーク接続のプール:各acquireが1つの接続を取り、releaseがそれをプールに戻します。
カウンティングセマフォは、外部サービスへのアクセス速度を制限し、スレッドプールを実装するために不可欠です。手動のスレッド管理なしで並列度を正確に制御できます。
| パラメータ | バイナリセマフォ | カウンティングセマフォ |
|---|---|---|
| 範囲 | 0または1 | 0からN |
| 同時スレッド数 | 1 | 最大N |
| 用途 | シグナリング、フラグ | リソースプール、レート制限 |
開発者はしばしばセマフォとミューテックスを混同しますが、両者には根本的な違いがあります。これらの違いを理解することは、プロジェクトで正しい同期メカニズムを選択するために非常に重要です。
重要な違いは所有権の概念です。ミューテックスは常にどのスレッドがそれを取得したかを把握し、そのスレッドだけが解放できます。セマフォには所有者がありません:任意のスレッドがacquireを呼び出さずにreleaseを呼び出せます。これにより、ミューテックスはデータ保護のためにより安全で、セマフォは調整のためにより柔軟になります。
実際には、ミューテックスは典型的なシナリオの最適化により、単純な相互排他でより高速です。セマフォはカウンタを維持するための追加オーバーヘッドを必要とします。ただし、並列性を制限したり、プロデューサー-コンシューマーパターンを実装するには、セマフォが不可欠です。
| 特性 | Semaphore | ミューテックス |
|---|---|---|
| 所有権 | 所有者なし | 所有者あり |
| 解放 | 任意のスレッド | 所有者スレッドのみ |
| カウンタ | 0からN | バイナリ |
| ユースケース | 並列性制限とシグナリング | クリティカルセクション保護 |
| 再帰 | なし | あり(再入可能) |
モバイルアプリケーション開発では、Semaphoreを使用して限られたリソース(ネットワーク接続、ファイル、データベース、ハードウェアコンポーネント)へのアクセスを管理します。現代のプラットフォームは便利な組み込み実装を提供しています。
典型的なユースケースの一つはHTTP接続プールです。プロバイダーのAPIが並列性を制限するため、アプリケーションはサーバーに同時に最大4つのリクエストしか送信できません。初期値4のセマフォは、どのような負荷でも同時リクエスト数が制限を超えないことを保証し、他のスレッドはキューで待機します。
セマフォがない場合、ユーザーアクティビティの急増によりサーバーインフラに突然の過負荷が発生し、タイムアウトや429 Too Many Requestsエラーが発生する可能性があります。Semaphoreはヒューズのように機能し、アクティブなスレッド数に関係なく、厳密に指定された数の同時呼び出しを許可します。
Androidはjava.util.concurrentパッケージからSemaphoreクラスを提供しています。サーバーの過負荷を防ぐために、同時ネットワークリクエストを2スレッドに制限する例を見てみましょう。
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
iOSでは、GCDのDispatchSemaphoreが同じタスクを解決します。開発者はメインスレッドをブロックせずに非同期コードでリソースへのアクセスを同期するためにこれを使用します。
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
最も一般的なエラーは、例外発生時のrelease忘れです。releaseを呼び出す前にスレッドがエラーで終了すると、セマフォは他のスレッドに対して永久にブロックされたままになります。確実な解放のためにtry/finallyまたはdeferを使用してください。2つ目の問題は、異なるスレッドが異なる順序で複数のセマフォを取得する際のデッドロックです。
セマフォはデータ保護だけでなく、複雑なマルチスレッドシナリオにおけるスレッド調整にも使用されます。一般的なパターンを理解することで、開発が加速され、同期エラーの可能性が減少します。
実際のプロジェクトでは、セマフォの使用にはいくつかの実績のあるパターンがあります。これらを知ることで、典型的な間違いを避け、信頼性の高いマルチスレッドシステムを構築できます。
初期値Nとタイマーによる定期的なreleaseを持つセマフォは、APIリクエストのレート制限を実装します。例えば、サービスが毎秒10リクエストを許可する場合:セマフォは10から開始し、各リクエストがカウンタを減少させ、別のTimerTaskが毎秒カウンタを初期値に戻します。これにより、アプリケーションとサーバーの両方を過負荷から保護します。
古典的なプロデューサー-コンシューマー問題では、2つのセマフォがバッファを管理します:empty(書き込み許可)とfull(読み取り許可)。プロデューサーはemptyでacquire、fullでreleaseを呼び出し、コンシューマーはその逆を行います。このスキームにより、コンシューマーが空のバッファを読み取らず、プロデューサーがバッファをオーバーフローさせないことが保証されます。
この同じスキームは、オペレーティングシステムのバウンデッドバッファ—固定サイズのリングバッファの基礎となっています。モバイルアプリケーションでは、このパターンは画像、ビデオファイル、分析イベントのキュー処理に使用されます。
セマフォはバックグラウンドサービスでのネットワーク呼び出しのスロットリングに成功裏に使用されています。例えば、分析アプリケーションがサーバーにイベントパケットを送信する場合、ピーク負荷時(アプリ起動、オフライン後の同期)に同時スレッドを制限しないと、同時リクエスト数がサーバー制限を超える可能性があります。初期値3のセマフォはスムーズな送信を保証し、サーバー側のブロッキングを防ぎます。
よくある質問
Semaphoreは単なるカウンタではなく、アトミック操作と待機キューを持つ同期プリミティブです。通常のカウンタはスレッドをブロックせず、複数のスレッドが同時にアクセスした場合のアトミックなインクリメントを保証しません。
はい、異なるスレッドが異なる順序で複数のセマフォを取得する場合にデッドロックが発生する可能性があります。例えば、スレッドAがS1を取得し、次にS2を取得する一方、スレッドBがS2を取得し、次にS1を取得する場合です。プロジェクト内のすべてのセマフォに対して単一の取得順序を固定してください。
スレッドはブロックされ、待機状態に入ります。別のスレッドがreleaseを呼び出すまでCPU時間を消費しません。JavaではBLOCKED状態、SwiftではGCDによってスレッドが一時停止されます。
主な違いは所有権です。Mutexは所有者スレッドによってのみ解放できます。Binary Semaphoreは任意のスレッドによって解放でき、スレッド間のシグナリングに便利ですが、データ整合性の保護には安全性が低くなります。
初期値はシナリオによって異なります。単一リソースの保護— 1。N接続のプール— N。スレッド間のシグナリングには、コンシューマースレッドがプロデューサーからのシグナルを待つように0を使用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。