###前段
私は長年PHPを主言語としていたせいか、スレッドの理解を苦手としてきました。
変数によるメモリ配置の違いなど考えたことはなく、JavaScriptでPromiseを学んだときも、「これが所謂マルチスレッドの動作なんだな、完全に理解した」などと思っていた気がします。
そのため、Javaなど静的片付け言語を学ぶ過程で、スレッドとメモリの理解不足で「なんか変数の値を壊してしまう恐れ」があると知ったときは衝撃でした。
私「動けば良かろうなのだ」
お客さん「なんか表示がおかしくなってるんですが」
私「」
実際のやりとりではありませんが、このようになる可能性がありました。
(Struts2.xのコントローラでは、リクエストごとにインスタンスが作られるので、大丈夫なようです)
実際、これまでも意識した経験が少なかったのですが、
最近、Kotlinで書くシステムのバッチ業務で、処理を並行化させる必要が生じ、コルーチンの存在を知りました。
静的型付け言語の非同期処理だから気をつけねばと身構えつつも、怪しかったスレッドセーフ周りの知識を補強し、使ってみることにしました。
私のゆるふわ度
Javaの例になりますが、マルチスレッドにおける
インスタンス変数など、共有資源の排他制御について、以下の要点を抑えました。
-
読み取りだけする場合
- 共有資源を不変(イミュータブル)にする
-
読み取り・更新をする場合
- synchronizedを使い、共有資源へのアクセスをロックする
- ThreadLocalを使い、共有しないようにする
synchronizedを使う場合はロックされる範囲・時間に気をつけ、
ThreadLocalを使う場合はGC管理に気をつけます。
詳しい内容は、別途調べて頂きたく思います。語れるだけの知識・経験がない…。
###使ってみる
で、身構えながらKotlin Coroutinesを使ってみたところ…。想像以上に民主的でした。
というか、デフォルトがmainスレッドだけで動く並行処理で、特にスレッドセーフの考慮がいらなかったりしました。
※Android版だと、UIの動くmainスレッドで動かすのは推奨されないので、少し事情が変わるようです。
Kotlin公式サイトのドキュメントを元にしたソースで、結果を確かめてみました。
package CoroutineTest
import kotlinx.coroutines.*
import kotlin.system.*
suspend fun 大量呼び出し(処理: suspend () -> Unit) {
val n = 100 // コルーチン起動回数
val k = 1000 // 処理実行回数
val time = measureTimeMillis {
coroutineScope { // scope for coroutines
repeat(n) {
launch {
repeat(k) { 処理() }
}
}
}
}
println("supend関数を ${n * k} 回実行しました")
}
var counter = 0 //スレッドセーフでないグローバル変数
fun main() = runBlocking {
//デフォルト実行
launch {
大量呼び出し {
counter++
println("[${Thread.currentThread().name}]")
}
}.join()
println("Counter = $counter")
}
mainスレッド上で動作し、カウンタは正しくカウントアップされます。
繰り返し試行しても、問題なさそうでした。
シングルスレッドで動くNode.jsのPromise,async/awaitと似ていますよね。
マルチスレッド基盤で動くWebアプリケーションなどを作らないなら、
コルーチンは特にスレッドのしくみを知らなくても安全に並行処理を使わせてくれるのでした。優しみを感じます。
そしてコルーチンは、別スレッドで起動し、並列処理をさせることもできます。
withContext(Dispatchers.Default) {
大量呼び出し {
counter++
println("[${Thread.currentThread().name}]")
}
}
println("Counter = $counter")
想定通り、正しくカウントアップされませんでした。
これをスレッドセーフにするには、synchronizedを使ってロックしたら良いでしょうか?
調べたところ、コルーチンスコープから参照する共有資源の排他制御では、Mutexが推奨されています。
Mutex.withLockはsupend関数であり、synchronizedとは違い、非ブロッキングに排他制御を実現します。
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
〜
〜
var counter = 0 //スレッドセーフでないグローバル変数
val mutex = Mutex()
fun main() = runBlocking {
var startTime = System.currentTimeMillis();
withContext(Dispatchers.Default) {
大量呼び出し {
mutex.withLock {
counter++
println("[${Thread.currentThread().name}]")
}
}
}
println("Counter = $counter")
println("処理時間 ${System.currentTimeMillis() - startTime}ミリ秒")
}
メモリの静的領域上の、Counter変数へのマルチスレッド処理がスレッドセーフになりました。
synchronizedでも試してみます。
var counter = 0 //スレッドセーフでないグローバル変数
val lock = Any()
fun main() = runBlocking {
var startTime = System.currentTimeMillis();
withContext(Dispatchers.Default) {
大量呼び出し {
synchronized(lock){
counter++
}
println("[${Thread.currentThread().name}]")
}
println("Counter = $counter")
println("処理時間 ${System.currentTimeMillis() - startTime}ミリ秒")
}
今回のコードでは、synchronizedを使う方が3倍程速い結果となりました。
ただしsynchronizedではスレッドをブロックしてしまうので、一つ一つに長時間を要する処理だと、suspendを使った方が良いのでしょう。
synchronizedとMutexの比較は、こちらの記事が大変参考になりました。
実際に動かして比較もできます。
https://jacquessmuts.github.io/post/coroutine_sync_mutex/
所感
ほかに、コルーチンを使っていて感じたのは、コルーチンスコープをすぐ書けることに加え、suspend関数に処理を切り出せることで、並行・並列化コードの分割が簡単にできるということ。
つまりローカル変数として扱える範囲がかんたんに広げられるので、実はあんまりスレッドセーフ気にせずに書けるということ。この辺り、JAVA8以降でラムダやStreamの関数が推されている理由でもあるみたいですね。
自分のようにスレッド理解のゆるふわな者でも、
割と安心して使えるといういまどきの流れは、スゴく助かるように感じました!
またデフォルトのmainスレッドだけで動かしても、よくあるhttpやdatabaseなどのI/Oを非同期にやりとりする用途の場合は十分のような気がします。
一方、CPU資源の有効活用をしたい場合でも、マルチスレッドで動かすことができます。
私の理解ではほぼ隙が見当たらず、Kotlin Coroutinesおすすめだと思います!
それではメリークリスマス