🦔
kotlin で 排他処理 mutex の活用
背景
kotlin で 排他処理をしたい時どうすればいいんだろう?
複数のコルーチン間でリソースの競合を防ぎたい
アプローチ① mutex
mutex をつかうと、コードブロック単位で実行するかどうかを選ぶことができます
lock と unlock
まずは mutex の基本的な lock() と unlock() の挙動を見てみましょう。
※ lock() と unlock() は mutex の状態を見てエラーを返してくれるので catch はしっかりと行いましょう
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
suspend fun normalLockUnlock(mutex: Mutex) {
mutex.lock()
try {
println("Critical section")
} finally {
mutex.unlock()
println("Mutex unlocked successfully")
}
}
Scenario 1: Normal lock/unlock
Critical section
Mutex unlocked successfully
suspend fun unlockWithoutLock(mutex: Mutex) {
try {
mutex.unlock()
println("This line should not be printed")
} catch (e: IllegalStateException) {
println("Caught IllegalStateException: ${e.message}")
}
}
Scenario 2: Unlock without locking
Caught IllegalStateException: This mutex is not locked
suspend fun doubleUnlock(mutex: Mutex) {
mutex.lock()
try {
println("Critical section")
mutex.unlock()
println("First unlock successful")
mutex.unlock()
println("This line should not be printed")
} catch (e: IllegalStateException) {
println("Caught IllegalStateException: ${e.message}")
}
}
Scenario 3: Double unlock
Critical section
First unlock successful
Caught IllegalStateException: This mutex is not locked
suspend fun unlockDuringCancellation(mutex: Mutex) = coroutineScope {
val job = launch {
mutex.lock()
try {
println("Critical section")
delay(1000) // 長時間の処理をシミュレート
} finally {
try {
mutex.unlock()
println("Mutex unlocked successfully during cancellation")
} catch (e: Exception) {
println("Exception during unlock: ${e.message}")
}
}
}
Scenario 4: Unlock during cancellation
Critical section
Mutex unlocked successfully during cancellation
withLock() と tryLock()
withLockの利点:
- 安全性: withLockは自動的にロックの取得と解放を行うため、ロックの解放忘れを防ぐことができます。
- 例外安全性: クリティカルセクション内で例外が発生しても、withLockは確実にロックを解放します。
- 可読性: コードがより簡潔で理解しやすくなります。
- デッドロック防止: ロックの取得と解放が明確に対応しているため、デッドロックのリスクが低減されます。
以下に、withLockがロックを取得するまで待機し、ロックが解放されるとwithLockセクションに入ることを示しています。
これにより、Mutexを使用して複数のコルーチン間でリソースの競合を防ぐことができます。
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun main() = runBlocking {
val mutex = Mutex()
val job1 = launch {
mutex.withLock {
println("Job 1: Lock acquired")
delay(2000) // クリティカルセクションをシミュレート
println("Job 1: Lock released")
}
}
val job2 = launch {
delay(500) // Job 1 がロックを取得する時間を与える
println("Job 2: Attempting to acquire lock")
mutex.withLock {
println("Job 2: Lock acquired")
// クリティカルセクション
}
println("Job 2: Lock released")
}
job1.join()
job2.join()
}
Job 1: Lock acquired
┗ Job 1 がロックを取得: Job 1: Lock acquiredが出力され、Job 1がロックを取得してクリティカルセクションに入る。
Job 2: Attempting to acquire lock
┗ Job 2 がロックを取得しようとする: Job 2: Attempting to acquire lockが出力され、Job 2がロックを取得しようとするが、Job 1がロックを保持しているため待機する。
Job 1: Lock released
┗ Job 1 がロックを解放: Job 1: Lock releasedが出力され、Job 1がクリティカルセクションを終了してロックを解放する。
Job 2: Lock acquired
┗ Job 2 がロックを取得: Job 2: Lock acquiredが出力され、Job 2がロックを取得してクリティカルセクションに入る。
Job 2: Lock released
┗ Job 2 がロックを解放: Job 2: Lock releasedが出力され、Job 2がクリティカルセクションを終了してロックを解放する。
tryLockの使用場面:
tryLockは特定の状況下で有用ですが、一般的にはwithLockほど安全ではありません。tryLockは以下のような場合に使用を検討できます:
- ノンブロッキングな動作が必要な場合
- タイムアウト付きのロック取得が必要な場合
- 複数のリソースに対する条件付きロックが必要な場合
自身で unlock() の処理をする必要があるのが withLock() との違いです
val mutex = Mutex()
suspend fun performOptionalOperation() {
if (mutex.tryLock()) {
try {
// クリティカルセクションのコード
} finally {
mutex.unlock()
}
} else {
// ロックが取得できなかった場合の処理
}
}
Discussion