Wander Purrgraming

kotlin mutexを使って排他制御する

1. 概要

Kotlinでは、マルチスレッド環境やコルーチンを使用する際にスレッド安全性を確保する必要があります。特に、複数のコルーチンやスレッドが同じリソースにアクセスすると、データ競合や不整合が発生する可能性があります。これを防ぐために Mutex(ミューテックス)を使用します。 Mutexを使用する際に詰まった点もあったので簡単にシェアしておこうと思います。

Mutexとは?

Mutexは、共有リソースへの排他制御を提供する仕組みです。synchronized ブロックと異なり、コルーチンに適した設計がされています。

2. Mutexを使わない場合の問題

まず、Mutex を使用しない場合のデータ競合の問題を確認してみましょう。

fun main() = runBlocking {
var counter = 0
runBlocking {
repeat(100000) {
launch(Dispatchers.Default) {
counter += 1
}
}
}
println("Counter: $counter")
}

このコードでは、counter を100,000回インクリメントするので直感的には最後に100000が表示されると思いますが、実際には異なる結果(例: 98,441)になります。これは counter += 1 の処理がスレッドセーフではなく、複数のスレッドが同時に counter を更新する際に競合が発生するためです。その結果、一部の更新が失われ、期待値より小さい数値が出力されます。

3. withLock の活用

Kotlinでは withLock を使用することで、安全で簡単にリソース保護が可能です。

fun main() {
var counter = 0
val mutex = Mutex()
runBlocking {
repeat(100000) {
launch(Dispatchers.Default) {
mutex.withLock {
counter += 1
}
}
}
}
println("Counter: $counter")
}

withLock で囲ったブロックが同時に1つしか処理されないことを保証してくれるので、データ競合が防がれ、Counter は期待通りの100000になります。 同時に実行されそうになった場合、後に実行を開始したほうを先に実行が開始されたものが終わるまで待機させてくれます。

試しにmutex.withLockの中にコメントを追加して挙動を確認してみます。

mutex.withLock {
println("lock start")
counter += 1
println("lock released")
}

すると

lock start
lock released
lock start
lock released
lock start
lock released

のような結果が得られるので、ロックが解放されるまで他のスレッドの処理が待機していることがわかると思います。

withLock を使うメリット

  • lock() / unlock() の明示的な呼び出しが不要
  • ロックの取得と解放のミスを防ぐ
  • コードがシンプルで読みやすい

4. Mutex のシングルトン性の重要性

他の記事ではあまり書かれてなかったリンするんですが、Mutex は スレッド制御するスコープ単位でインスタンスが同じである必要があります。 各コルーチンが異なる Mutex インスタンスを使用すると、ロックが適切に機能せず、データ競合が発生します。そのため、Mutex はシングルトンとして管理することが推奨されます。

kotlinでは object を使うことで簡単にシングルトンパターンの実装が可能です。

object TestRepository {
val mutex = Mutex()
suspend fun getInfo() {
mutex.withLock {
getInfoFromLocalDatasource() ?: getInfoFromRemoteDatasource()
}
}
private suspend fun getInfoFromRemoteDatasource() Info {
// APIから値を取得する処理など
}
private suspend fun getInfoFromLocalDatasource(): Info? {
// キャッシュから値を取得する処理など
}
}

引数を受け取る必要がありクラスを使いたい場合はmutexインスタンスがシングルトンであればいいのでcompanion objectを使うのも手です。 自分は元々companion object外で普通にプロパティとして持ってしまっていたのですが、クラスのインスタンス毎に別々にmutexがインスタンス化されてしまって、うまく排他制御が効いてくれませんでした(泣)

class TestRepository(
cacheInfo: CacheInfo // キャッシュ時間など
) {
suspend fun getInfo() {
mutex.withLock {
getInfoFromLocalDatasource() ?: getInfoFromRemoteDatasource()
}
}
private suspend fun getInfoFromRemoteDatasource() Info {
// APIから値を取得する処理など
}
private suspend fun getInfoFromLocalDatasource(cacheInfo: CacheInfo): Info? {
// キャッシュから値を取得する処理など
}
companion object {
val mutex = Mutex()
}
}

このようにmutexインスタンスがシングルトンになるように適切に管理しましょう!

5. パフォーマンスと注意点

Mutex のオーバーヘッド

Mutex はスレッドセーフですが、ロックによるパフォーマンス低下を招く可能性があります。過度に使用すると並列処理のメリットが損なわれるため、最小限に抑えることが重要です。

6. まとめ

  • Mutex はコルーチン環境でのスレッド安全性を確保するために有用
  • withLock を活用するとミスを防げる
  • Mutex はシングルトンとして扱い、適切に共有することが重要
  • パフォーマンスに注意し、ロックの使用を最小限に抑えることが大切