- PVSM.RU - https://www.pvsm.ru -
Предположим, у вас в коде есть критическая секция [1], которая не должна выполняться более, чем одним потоком одновременно.
В мире Java одним из стандартных решений является добавление ключевого слова synchronized к сигнатуре метода. В Kotlin для получения того же эффекта используется аннотация @Synchronized
repeat(2) {
thread { criticalSection() }
}
@Synchronized
fun criticalSection() {
println("Starting!")
Thread.sleep(10)
println("Ending!")
}
Данный код выведет следующее:
Starting!
Ending!
Starting!
Ending!
Видно, что оба вызова отработали последовательно, один после другого.
Теперь предположим, что мы хотим использовать корутины. Что у нас получится?
val scope = CoroutineScope(Job())
repeat(2) {
scope.launch { criticalSectionSuspending() }
}
@Synchronized
suspend fun criticalSectionSuspending() {
println("Starting!")
delay(10)
println("Ending!")
}
А получится, что вызовы критической секции пересекутся, что очень не здорово.
Starting!
Starting!
Ending!
Ending!
Понять, что же происходит, можно, разобравшись, как устроены корутины под капотом. Они реализованы с использованием подхода передачи продолжения [2]. (Краткое объяснение можно посмотреть в моём докладе про корутины Grokking Coroutines, Dan Lew [3], а для более глубокого понимания рекомендую посмотреть доклад Романа Елизарова Deep Dive into Coroutines [4].)
В рамках этой статьи всё, что вам надо знать — это то, что останавливаемые функции [5] на самом деле не исполняются последовательно, строка за строкой. Когда останавливаемая функция доходит до точки остановки, она ставится на паузу и передаёт управление какой-то другой функции (позже, когда управление будет возвращено этой функции, — она продолжит выполнение).
Таким образом, во втором примере кода на самом деле происходит следующее:
1. criticalSectionSuspending() стартует, забирает блокировку и печатает Starting!
2. Доходит до delay() (который является точкой остановки), выходит из функции и отдаёт блокировку.
3. Так как блокировка свободна, начинается второй запуск criticalSectionSuspending(), которая забирает блокировку, печатает Starting!, останавливается и тоже отдаёт блокировку.
4. Когда delay() заканчивается, criticalSectionSuspending() запускается снова, но уже с предыдущего места остановки.
Для большей наглядности привожу временную диаграмму выполнения одной функции.
Как видите, время остановки проходит вне критической секции и её блокировка не удерживается. Именно поэтому разные потоки имеют доступ к синхронизированной останавливаемой функции — они выполняют её не одновременно.
Это известная проблема в Kotlin. Фактически, компилятор не позволит использовать synchronized() {} с точкой останова внутри. Такой код не скомпилируется с ошибкой "The 'delay' suspension point is inside a critical section"
Я убеждён, что в случае использования аннотации @Synchronized поведение компилятора должно быть аналогичным. Проблема заведена в YouTrack [6], но особых подвижек пока нет.
Как же быть?
Во-первых, следует признать, что проблема не в том, что «мы не можем использовать synchronized». synchronized — это просто средство обеспечения работоспособности критических секций. И единственная причина того, что у нас есть эти критические секции в том, что у нас есть общее изменяемое состояние. Соответственно, проблема звучит следующим образом: «нам нужен способ управления общим изменяемым состоянием в многопоточной среде».
К счастью, у нас есть официальное руководство по Kotlin, в котором есть раздел Shared mutable state and concurrency [7], описывающий несколько неплохих вариантов действий. Для нас более всего подходит секция про мьютексы [8], так как они наиболее похожи на синхронизацию.
val mutex = Mutex()
val scope = CoroutineScope(Job())
repeat(2) {
scope.launch { criticalSectionSuspendingLocked() }
}
suspend fun criticalSectionSuspendingLocked() {
mutex.withLock {
println("Starting!")
delay(10)
println("Ending!")
}
}
Код выше работает ровно так, как нам нужно, — выводит не перемешанные сообщения.
Я не хочу, чтобы люди считали, что использование мьютексов является универсальным решением при работе с общим изменяемым состоянием. В документации описано несколько подходов, и вы должны самостоятельно оценить, какой из них подходит к вашей конкретной ситуации. Но в случае, когда вы использовали синхронизированную функцию и теперь хотите её приостановить, мьютекс — это самый естественный выбор.
От переводчика:
Эту статью я нашёл во время поисков ответа на вопрос: Почему мьютекс в моём конкретном случае работает сильно медленнее (в полтора - два раза), чем синхронизация? Просто имейте в виду, если у вас нет точек останова в горячей критической секции, то лучше использовать синхронизацию, так как мьютекс может сильно просадить производительность.
Автор: Громов Андрей
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/358981
Ссылки в тексте:
[1] критическая секция: https://ru.wikipedia.org/wiki/%D0%9A%D1%80%D0%B8%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%B5%D0%BA%D1%86%D0%B8%D1%8F
[2] передачи продолжения: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B4%D0%BE%D0%BB%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)
[3] Grokking Coroutines, Dan Lew: https://www.youtube.com/watch?v=Axq8LdQqQGQ&abchannel=TwinCitiesKotlinUsers
[4] Deep Dive into Coroutines: https://www.youtube.com/watch?v=YrrUCSi72E8&abchannel=JetBrainsTV%5D
[5] останавливаемые функции: https://kotlinlang.ru/docs/reference/coroutines.html
[6] YouTrack: https://youtrack.jetbrains.com/issue/KT-27333
[7] Shared mutable state and concurrency: https://kotlinlang.org/docs/reference/coroutines/shared-mutable-state-and-concurrency.html
[8] мьютексы: https://kotlinlang.org/docs/reference/coroutines/shared-mutable-state-and-concurrency.html#mutual-exclusion
[9] Источник: https://habr.com/ru/post/528778/?utm_source=habrahabr&utm_medium=rss&utm_campaign=528778
Нажмите здесь для печати.