Введение в синхронизацию в Kotlin
Несколько корутин могут одновременно читать и менять
одну переменную. Каждый шаг "прочитать - прибавить -
записать" выглядит атомарным только в исходнике:
между чтением и записью другая задача успевает сделать
то же самое, и часть приращений теряется.
Гонка проявляется на общем счетчике, когда
две фоновые корутины много раз увеличивают одно число.
После join итог часто меньше ожидаемой суммы
шагов, хотя каждая итерация выглядит простой.
Проверка на двух задачах по тысяче приращений без
защиты обычно дает число ниже 2000:
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
runBlocking {
var hits = 0
val jobs = List(2) {
launch {
repeat(1000) {
hits++
}
}
}
joinAll(*jobs.toTypedArray())
println(hits)
}
}
Mutex из kotlinx.coroutines.sync
держит очередь: в критический участок входит одна
корутина, остальные ждут на приостановке, пока замок
не освободится. Короткое приращение под замком
выглядит так:
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
fun main() {
runBlocking {
var hits = 0
val guard = Mutex()
val jobs = List(2) {
launch {
repeat(1000) {
guard.lock()
try {
hits++
} finally {
guard.unlock()
}
}
}
}
joinAll(*jobs.toTypedArray())
println(hits)
}
}
После ожидания обеих задач печать чаще показывает
ровно 2000. Замок не ускоряет работу, а
гарантирует, что приращения не накладываются друг
на друга в один момент времени.