初めて遭遇し焦ったものの、使い方自体はさほど難しくないwithLockについてまとめました。
Javaのパッケージが関連しているためJavaで調べると欲しい情報が見つけやすいこと、早めに気づきたかったです。
仕様
与えられた処理をロック下で実行するための関数です。
引数にロック下で行いたい処理 actionをとり、actionの戻り値を返します。
action実行前に自動でロックされた後actionを実行し、その後自動でロックが解除されます。
inline fun <T> Lock.withLock(action: () -> T): T
ロック下とは、排他制御下であるという意味です。
排他制御とは、同じ資源を同時に利用すると不都合が起きる場合に、同時アクセスを禁止して整合性を保つ仕組みを指します。
例えば、共同で使用している銀行口座から同時に2人がお金をおろしてしまうと残高が合わなくなる、そのようなことを防ぐために使われている仕組みです。
withLockは、Kotlinの標準ライブラリkotlin-stdlib
に含まれる並列プログラミングのためのユーティリティ関数パッケージkotlin.concurrent
にて提供されています。
ちなみに、並列プログラミングとは同時に複数の処理を実行することで、高いパフォーマンスを実現するためのプログラミングの手法です。
Androidでよく目にする「マルチスレッド」も並列プログラミングの考え方の1つであり、実装で利用されるthread{ }
もこのパッケージで提供されています。
使用方法
withLockを使用する際には、withLockを使えるように環境を整え、ブロック内に行いたい処理を記述します。
具体的には、以下の手順で行います。
①build.gradleに依存関係を追加
②kotlin.concurrent.withLock
をインポート
③java.util.concurrent.locks.ReentrantLock()
を呼び出す
④withLockを記述し、ブロック内にロック下に行いたいアクションを記述
dependencies {
// ①kotlin-stdlibライブラリを追加
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.20"
}
import kotlin.concurrent.withLock // ②withLockをインポート
private val lock = java.util.concurrent.locks.ReentrantLock() //③ 毎回呼ぶには長いためlockという定数に代入
class Main(){
fun main(){
lock.withLock {
// ④withLockを実装し、行いたい処理を記述
}
}
}
ちなみにReentrantLock()を使う別の方法としてlock()とunlock()を使う方法もありますが、こちらはどちらかを記述し忘れてしまうとエラーが発生します。
その点withLockでは、処理の実行前/実行後に自動でロック/ロック解除される仕様のため、抜け漏れの心配をせず安心して使うことができます。
実装例
使い方は難しくありませんが、初心者には具体的にどのような場合に使えば良いが分かりにくいのがwithLockです。
今回は以下の2種類の場合について、withLockを使う場合/使わない場合で結果がどのように変換するかを確認します。
- 引き算を行う例
- 順番通りに出力する例
1. 引き算を行う例
1つ目は、排他制御の説明で紹介した、銀行残高からの引き落としとその結果を出力する処理です。
この処理では元々1万円あった残高をAさん〜Eさんの5人がそれぞれ300〜1500円ずつ使った場合に、残高が正しく計算されていることを確認します。
- withLockを使わない場合
private fun main() {
val name = listOf<String>("A", "B", "C", "D", "E") // 名前のリスト
for(i in 1..5){ // for文で5本のスレッドを作成
thread {
withdraw(name[i - 1], 300 * i) // Aさん〜Eさんの使用金額とその時の残高を計算
Thread.sleep(50) // 出力待ちなどで50ミリ秒の間このスレッドが一時中断することを再現
}
}
}
private var balance: Int = 10000
private fun withdraw(name: String, used: Int){
balance -= used // 残高から使用金額を引く
Log.d("結果", "${name}さんが${used}円利用。残:${balance}円")
}
// 結果
Cさんが900円利用。残:9100円
Eさんが1500円利用。残:6400円
Dさんが1200円利用。残:6400円
Bさんが600円利用。残:5500円
Aさんが300円利用。残:6100円
- withLockを使う場合
private fun main() {
val lock = java.util.concurrent.locks.ReentrantLock()
val name = listOf<String>("A", "B", "C", "D", "E")
for(i in 1..5){
thread {
lock.withLock { // withLockで排他制御をかける
withdraw(name[i - 1], 300 * i)
Thread.sleep(50)
}
}
}
}
// withdrawメソッドはwithLockを使わない場合と同じため省略
// 結果
Aさんが300円利用。残:9700円
Bさんが600円利用。残:9100円
Cさんが900円利用。残:8200円
Dさんが1200円利用。残:7000円
Eさんが1500円利用。残:5500円
withLockを使わない場合は計算結果が毎回異なり、残高の計算が意図しない結果になりました。
一方で、withLockを使う場合は排他制御が行われ、利用額と毎回の残高が正確に計算されていることが分かります。
2. 順番通りに出力する例
2つ目の例は、1-1, 1-2, … , 5-2を出力する処理です。
一時中断される処理がある場合でも、意図した順序で実行できるかを確認します。
- withLockを使わない場合
private fun main() {
for(i in 1..5){
thread {
print("${i}-1 ")
Thread.sleep(50) // 50ミリ秒の間このスレッドを一時中断する
print("${i}-2\n")
}
}
}
// 結果
I/System.out: 2-1 1-1 4-1 5-1 3-1 2-2
I/System.out: 1-2
I/System.out: 4-2
I/System.out: 5-2
I/System.out: 3-2
- withLockを使う場合
private fun main() {
val lock = java.util.concurrent.locks.ReentrantLock()
for(i in 1..5){
thread {
lock.withLock { // withLockで排他制御をかける
print("${i}-1 ")
Thread.sleep(50) // 50ミリ秒の間このスレッドを一時中断する
print("${i}-2\n")
}
}
}
}
// 結果
I/System.out: 1-1 1-2
I/System.out: 2-1 2-2
I/System.out: 3-1 3-2
I/System.out: 4-1 4-2
I/System.out: 5-1 5-2
withLockを使わない場合は、一時中断される直前までの処理が次々と実行されています。
一方でwithLockを使う場合は、排他制御が行われるためスレッドが実行されている間の割り込みができず、スレッド内の処理がひとまとまりで実行されていることが分かります。
以上より、withLockを使うことで、
①他のスレッド情報の影響を受けず
②割り込みの心配をせずにひとまとまりで
処理を実行できることが分かりました。
withLockを使いこなして、①や②を満たす楽しい排他制御ライフを送りましょう!
参考
・Kotlin: withLock
・KotlinのConcurrencyライブラリを使う - 算譜王におれはなる!!!!
・ReentrantLockで順序性を保証するロック
・ラムダ式の基本 (lambda expression) | まくまくKotlinノート
・Javaのsleep機能 Tread sleepメソッドとは?基本情報をご紹介Javaコラム