Mutex(相互排他)は、任意の時点で1つのスレッドのみがコードのクリティカルセクションを実行できることを保証する同期プリミティブです。Microsoft Docs (Synchronization Objects, 2024)によると、Mutexの基本原則は所有権です:Mutexを取得したスレッドがその所有者となり、クリティカルセクションを抜けるときにのみ解放します。Mutexは、レースコンディション(Race Condition)を防止し、マルチスレッドアプリケーションでデータの整合性を確保するための基本的なツールです。
重要なポイント
Mutex(Mutual Exclusion — 相互排他の略)は、マルチスレッド環境で共有リソースへのアクセスを管理する同期オブジェクトです。スレッドがクリティカルセクションに入ると、Mutexを取得します。別のスレッドが同じMutexを取得しようとすると、最初のスレッドがロックを解放するまで待機状態になります。
Mutexのアーキテクチャは、1965年にEdsger Dijkstraによって設計されたTHEオペレーティングシステムにまで遡ります。Dijkstraはセマフォの概念を導入し、そこからMutexが特別なケースとして後に登場しました — 所有権サポート付きのバイナリセマフォです。現代のOS(Linux、Windows、Android)はカーネルレベルでMutexを実装しており、異なるプロセス間でも正しい同期を保証します。
Mutexの重要な特性は所有権(ownership)です。ミューテックスを取得したスレッドのみが解放できます。これによりMutexはバイナリセマフォと区別されます。バイナリセマフォでは任意のスレッドがシグナル(V操作)を実行できます。所有権により、別のスレッドによる誤ったロック解放が防止され、モバイル開発における一般的な同期シナリオでMutexがより安全になります。Android Developer Docs(Processes and Threads、2024)によると、高競合下でsynchronizedの代わりにMutexを使用すると、パフォーマンスが30%向上する可能性があります。
Mutexは2つの状態のいずれかです:ロック済み(locked) — スレッドによって取得済み;またはアンロック(unlocked) — 未取得。2つの基本操作があります:lock()(取得)とunlock()(解放)。Mutexがすでにロックされている場合、lock()を呼び出したスレッドはロックが解放されるまでブロックされます。JVMでは、ブロックされたスレッドはBLOCKED状態に遷移し、CPUを消費しません。
Mutexが解放されると、システムはどの待機スレッドがロックを取得するかを選択します。非フェア(non-fair)スケジューリングでは、選択はミューテックスを解放したばかりのスレッドに下される可能性があります — これによりスループットは向上しますが、スターベーション(枯渇)が発生する可能性があります。フェアスケジューラはFIFOキューを使用します:最初の待機スレッドが最初にロックを取得します。ReentrantLock(true)はまさにこのメカニズムを実装しています。
Java/KotlinのほとんどのMutex実装はリエントラント取得をサポートしています。スレッドがすでにMutexを所有していて再度lock()を呼び出すと、操作は成功します — Mutexは自分自身をブロックしません。再帰カウンタが増加し、スレッドはlock()と同じ回数だけunlock()を呼び出す必要があります。これは再帰呼び出しやネストされたクリティカルセクションにとって重要です。
典型的なタスクを考えてみましょう — ReentrantLock(Java/Kotlinの古典的なMutex)を使用して共有カウンタをレースコンディションから保護します。Mutexがないと、コードは誤った結果を生成します。Mutexがあれば、1000個すべてのスレッドが確実にカウンタ値を増加させます。
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // クリティカルセクション
} finally {
mutex.unlock() // 必須のfinally
}
}
fun getCount(): Int {
mutex.lock()
try {
return count
} finally {
mutex.unlock()
}
}
}
fun main() = runBlocking {
val counter = MutexCounter()
val jobs = List(1000) {
launch(Dispatchers.Default) {
counter.increment()
}
}
jobs.forEach { it.join() }
println(counter.getCount()) // 常に1000
}
finallyブロックに注意してください — Mutexを扱う際の必須パターンです。クリティカルセクション内で例外が発生すると、unlock()が呼び出されず、Mutexは永久にロックされたままになります — これによりデッドロックが発生します。finallyブロックは、セクションの実行がどのように終了するかに関係なく、Mutexの解放を保証します。
Kotlinでの代替アプローチは、withLock拡張関数を使用することです。これはfinallyを使用してlock/unlockを自動的に処理します。
fun increment() {
mutex.withLock { // lock + try/finally 自動的
count++
}
}
fun getCount(): Int = mutex.withLock { count }
これら3つの同期メカニズムはしばしば混同されますが、異なる特性と使用ケースがあります。Mutexは所有権を持つバイナリです。セマフォは所有権のない許可カウンタです。モニターはMutexと条件変数を組み合わせた高レベルメカニズムです。これらの違いを理解することは、特定のタスクに適したツールを選択するために極めて重要です。
| パラメータ | Mutex | セマフォ | モニター |
|---|---|---|---|
| タイプ | バイナリ (0/1) | カウント (0..N) | バイナリ + 条件 |
| 所有権 | 所有者のみunlock可能 | 任意のスレッドがsignal可能 | 所有者のみ |
| リエントランシー | 通常はい (reentrant) | いいえ | はい |
| 条件付き待機 | いいえ(Conditionが必要) | いいえ | 組み込み (wait/notify) |
| Java/Kotlinの例 | ReentrantLock | Semaphore(permits) | synchronized |
Mutexを選ぶべき時:単一のリソースを同時アクセスから保護する必要がある場合 — 例えば、共有コレクション、ファイル、またはカウンタ。セマフォを選ぶべき時 — リソースプールへの同時アクセス数を制限する必要がある場合、例えば5接続のデータベース接続プール。モニターを選ぶべき時 — 条件付き待機による同期が必要な場合、例えばwait/notifyを使用したプロデューサー-コンシューマーキュー。現代のAndroid開発では、synchronizedはしばしばReentrantLockまたはkotlinx.coroutines Mutexに置き換えられます。
最も一般的なエラーは、unlock()を呼び出すためのfinallyブロックの欠落です。クリティカルセクションで例外が発生すると、Mutexはロックされたままになり、他のスレッドは永久に待機します。例外が絶対に起こらないと確信している場合でも — 常にtry/finallyまたはwithLockを使用してください。これは防御的プログラミングの原則であり、メモリ不足やConfiguration Changesによって例外が発生する可能性があるモバイル開発では特に重要です。
アプリケーションが複数のMutexを使用する場合、一貫した取得順序を確立することが極めて重要です。スレッドAがM1 → M2を取得し、スレッドBがM2 → M1を取得すると、デッドロックが発生します。大規模プロジェクト(5万行以上のコード)では、ロック順序はアーキテクチャ決定として文書化され、リンターによって検証されます。IntelliJ IDEAのLock Checkerツールは、不整合なロック取得順序を自動的に検出します。
Mutexを1〜2ミリ秒以上保持することは、設計不良の兆候です。クリティカルセクションには最小限の必要な操作のみを含めるべきです。ネットワークリクエスト、ファイルI/O、複雑な計算はロックされたブロックの外部で実行する必要があります。Androidでは、UIスレッドでの長時間のロック保持はフレームドロップ(ジャンク)やANRを引き起こします。クリティカルセクションが主に読み取り操作で構成されている場合は、ReadWriteLockを使用してください。
kotlinx.coroutinesライブラリは独自のMutex実装を提供し、これは古典的なReentrantLockとは根本的に異なります。主な違いは、suspending MutexがOSスレッドをブロックせず、ロックが解放されるまでコルーチンを一時停止することです。つまり、現在のコルーチンがMutexを待機している間、スレッドは他のコルーチンを実行できます。
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class CoroutineCounter {
private val mutex = Mutex()
private var count = 0
suspend fun increment() {
mutex.withLock { // suspending — スレッドをブロックしない
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
kotlinx Mutexの主な特徴:非リエントラント(non-reentrant) — ReentrantLockとは異なり、コルーチンはすでに所有しているMutexを再取得できません。これが必要な場合は、Mutexの代わりにSemaphore(1)を使用してください。さらに、kotlinx.coroutinesのMutexはノンブロッキングです:suspendによる一時停止を使用するため、プールスレッドをブロックしません。
実際には、コルーチンコードでは2つの理由でsuspending Mutexが古典的なReentrantLockより好まれます:スケーラビリティ — 1つのコルーチンがMutexを待機している間にスレッドが他のコルーチンを処理し、システムスループットが向上します;BlockedThreadがない — ブロックされたスレッドのスタックを保存するリソースが消費されません。JetBrains(Kotlin Coroutines Guide、2024)によると、100以上のコルーチンでsuspending Mutexを使用するとスループットが40%向上します。
よくある質問
所有権(ownership)が根本的な違いです。Mutexはどのスレッドが取得したかを記憶し、そのスレッドのみが解放できます。バイナリセマフォ(Semaphore(1))には所有者がいません — 任意のスレッドがrelease()を呼び出せます。したがって、Mutexの方が安全です:別のスレッドが誤って他人のロックを解放することはできませんが、セマフォでは可能です。
synchronizedはよりシンプルで短いです — タイムアウトや公平性制御のない単純なクリティカルセクションに使用してください。ReentrantLockは、タイムアウト付きTryLock、フェアスケジューリング、Condition Variables、または待機スレッドの割り込み(lockInterruptibly)が必要な場合に使用してください。コルーチンでは、常にkotlinx.coroutines.sync.Mutexを使用してください。
スピンロックは、スレッドがスリープせずにループ内でロック状態をチェックしながら回転(spin)するロックです。スピンロックはCPUを消費しますがコンテキストスイッチを行わないため、短いクリティカルセクション(最大10命令)に有利です。MutexはスレッドをBLOCKED状態にし、コンテキストスイッチのために10〜50マイクロ秒多くコストがかかりますが、CPUを無駄に消費しません。
Linuxカーネルレベルでは、Mutexはfutex(fast userspace mutex)を介して実装されています。スレッドはまず、アトミックCAS(Compare-And-Swap)命令を使用してユーザースペースでロックを取得しようとします。Mutexが空いている場合 — syscallなしで取得が行われます。ビジーの場合 — スレッドはsyscall futex(FUTEX_WAIT)を行いスリープします。解放時に、syscall futex(FUTEX_WAKE)が1つの待機スレッドを起こします。
はい、プロセス間Mutex(inter-process mutex)が存在します。WindowsではNamed Mutex、LinuxではPTHREAD_PROCESS_SHARED属性を持つpthread_mutexattr_setpsharedです。AndroidのBionic libcもファイル記述子を介してプロセス間Mutexをサポートしています。プロセス間Mutexは、異なるアプリケーション間、またはプロセスとその子プロセス間の同期に使用されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。