🦔

kotlin で 排他処理 mutex の活用

2024/07/31に公開

背景

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の利点:

  1. 安全性: withLockは自動的にロックの取得と解放を行うため、ロックの解放忘れを防ぐことができます。
  2. 例外安全性: クリティカルセクション内で例外が発生しても、withLockは確実にロックを解放します。
  3. 可読性: コードがより簡潔で理解しやすくなります。
  4. デッドロック防止: ロックの取得と解放が明確に対応しているため、デッドロックのリスクが低減されます。

以下に、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は以下のような場合に使用を検討できます:

  1. ノンブロッキングな動作が必要な場合
  2. タイムアウト付きのロック取得が必要な場合
  3. 複数のリソースに対する条件付きロックが必要な場合

自身で unlock() の処理をする必要があるのが withLock() との違いです

val mutex = Mutex()

suspend fun performOptionalOperation() {
    if (mutex.tryLock()) {
        try {
            // クリティカルセクションのコード
        } finally {
            mutex.unlock()
        }
    } else {
        // ロックが取得できなかった場合の処理
    }
}

Discussion