Сопрограммы Kotlin бывают раньше гарантий?

Предоставляют ли сопрограммы Kotlin какие-либо гарантии «случится до»?

Например, существует ли гарантия «происходит до» между записью в mutableVar и последующим чтением в (потенциально) другом потоке в этом случае:

suspend fun doSomething() {
    var mutableVar = 0
    withContext(Dispatchers.IO) {
        mutableVar = 1
    }
    System.out.println("value: $mutableVar")
}

Редактировать:

Возможно, дополнительный пример прояснит вопрос лучше, потому что он больше похож на Котлин (за исключением изменчивости). Является ли этот код потокобезопасным:

suspend fun doSomething() {
    var data = withContext(Dispatchers.IO) {
        Data(1)
    }
    System.out.println("value: ${data.data}")
}

private data class Data(var data: Int)

person Vasiliy    schedule 23.10.2019    source источник
comment
Обратите внимание, что при работе на JVM Kotlin использует ту же модель памяти, что и Java.   -  person Slaw    schedule 23.10.2019
comment
@Slaw, я знаю это. Однако под капотом происходит много волшебства. Поэтому я хотел бы понять, есть ли какие-то гарантии, которые я получаю от сопрограмм, или это все на мне.   -  person Vasiliy    schedule 23.10.2019
comment
Во всяком случае, ваш второй пример представляет собой еще более простой сценарий: он просто использует объект, созданный в withContext, тогда как первый пример сначала создает его, мутирует в withContext, а затем читает после withContext. Таким образом, первый пример реализует больше функций безопасности потоков.   -  person Marko Topolnik    schedule 23.10.2019
comment
... и оба примера реализуют только самый тривиальный аспект порядка выполнения программы. Я говорю здесь об уровне сопрограмм, а не о базовой JVM. Таким образом, в основном, вы спрашиваете, настолько ли сильно сломаны сопрограммы Kotlin, что они даже не обеспечивают порядок выполнения программы до того, как это произойдет.   -  person Marko Topolnik    schedule 23.10.2019
comment
@MarkoTopolnik, поправьте меня, если я ошибаюсь, но JLS гарантирует только порядок выполнения программы - до выполнения в том же потоке. Теперь, с сопрограммами, хотя код выглядит последовательным, на практике есть некий механизм, который разгружает его в разные потоки. Я понимаю вашу точку зрения, что это такая базовая гарантия, что я даже не стал бы тратить время на ее проверку (из другого комментария), но я задал этот вопрос, чтобы получить строгий ответ. Я почти уверен, что примеры, которые я написал, являются потокобезопасными, но я хочу понять, почему.   -  person Vasiliy    schedule 23.10.2019
comment
Вы должны думать на уровне сопрограмм. На этом уровне вы написали последовательный код, включающий одну сопрограмму (withContext просто продолжает выполнять ту же самую сопрограмму в другом контексте). Таким образом, вы не выполняете ничего, кроме согласованности порядка выполнения программы. Теперь есть две проблемы: 1. Гарантируют ли сопрограммы Kotlin согласованность порядка выполнения программы? 2. Поддерживает ли фактическая реализация эту гарантию? Я не уверен, какая из этих двух проблем вызывает у вас подозрения.   -  person Marko Topolnik    schedule 23.10.2019


Ответы (2)


Код, который вы написали, имеет три доступа к общему состоянию:

var mutableVar = 0                        // access 1, init
withContext(Dispatchers.IO) {
    mutableVar = 1                        // access 2, write
}
System.out.println("value: $mutableVar")  // access 3, read

Три доступа упорядочены строго последовательно, без параллелизма между ними, и вы можете быть уверены, что инфраструктура Kotlin позаботится об установлении края случается до при передаче в пул потоков IO и обратно в ваш вызов сопрограммы.

Вот эквивалентный пример, который, возможно, выглядит более убедительно:

launch(Dispatchers.Default) {
    var mutableVar = 0             // 1
    delay(1)
    mutableVar = 1                 // 2
    delay(1)
    println("value: $mutableVar")  // 3
}

Поскольку delay является приостанавливаемой функцией, и поскольку мы используем диспетчер Default, поддерживаемый пулом потоков, строки 1, 2 и 3 могут выполняться в разных потоках. Поэтому ваш вопрос о гарантиях произошло до в равной степени относится и к этому примеру. С другой стороны, в данном случае (надеюсь) совершенно очевидно, что поведение этого кода соответствует принципам последовательного выполнения.

person Marko Topolnik    schedule 23.10.2019
comment
Спасибо. На самом деле именно та часть после того, как вы были уверены, побудила меня задать этот вопрос. Есть ли ссылки на документы, которые я мог бы прочитать? В качестве альтернативы, ссылки на исходный код, где это происходит — до того, как установлен край, также будут очень полезны (соединение, синхронизация или любой другой метод). - person Vasiliy; 23.10.2019
comment
Это настолько базовая гарантия, что я даже не стал бы тратить время на ее проверку. Под капотом все сводится к executorService.submit() и есть какой-то типичный механизм ожидания завершения задачи (выполнение CompletableFuture или что-то подобное). С точки зрения сопрограмм Kotlin здесь вообще нет параллелизма. - person Marko Topolnik; 23.10.2019
comment
Вы можете думать о своем вопросе как о вопросе, гарантирует ли ОС, что произойдет до приостановки потока, а затем возобновления его на другом ядре? Потоки для сопрограмм — то же, что ядра ЦП для потоков. - person Marko Topolnik; 23.10.2019
comment
Спасибо за ваше объяснение. Однако я задал этот вопрос, чтобы понять, почему это работает. Я понимаю вашу точку зрения, но пока это не тот строгий ответ, который я ищу. - person Vasiliy; 23.10.2019
comment
Я думаю, вы на самом деле ищете что-то другое, кроме формальной строгости, потому что мы уже установили, что ваш код является последовательным и что сопрограммы Kotlin, вне всякого сомнения, обеспечивают гарантию «до того, как произойдет» для действий, упорядоченных по порядку программы. Возможно, вы ищете окончательное доказательство того, что реализация не сломана, но это довольно сложная задача, и вам придется постоянно переоценивать ее с каждой новой версией Kotlin. - person Marko Topolnik; 23.10.2019
comment
Ну... я на самом деле не думаю, что этот поток установил, что код является последовательным. Он утверждал это, конечно. Мне тоже было бы интересно увидеть механизм, который гарантирует, что пример ведет себя так, как ожидается, не влияя на производительность. - person G. Blake Meike; 23.10.2019
comment
@G.BlakeMeike Как вы думаете, чего не хватает? Ссылка на документацию по семантике withContext? Ваша вторая проблема, механизм, является деталью реализации, которая не имеет отношения к семантике. В любом случае, механизм специфичен для каждого диспетчера. - person Marko Topolnik; 23.10.2019
comment
@G.BlakeMeike without affecting performance -- конечно передача в другой поток и обратно влияет на производительность по сравнению с простым выполнением всего кода без прерываний. Это никто не оспаривает. - person Marko Topolnik; 23.10.2019
comment
@MarkoTopolnik Я не смог найти окончательного ответа на этот вопрос с помощью сопрограмм Kotlin или Java ExecutorService. Каждый пример, который я нашел, имеет перезаписываемую примитивную переменную. Как насчет косвенных членов объекта? Например, если мы заменим var mutableVar = 0 в вашем коде на val list = MutableList() и mutableVar = 1 на list.add(1), а затем напечатаем размер списка в последней строке кода, есть ли гарантия, что все состояние объекта произойдет раньше? Все ли члены списка действительно изменчивы? - person Tenfour04; 17.06.2021
comment
@ Tenfour04 Они не должны быть изменчивыми. При входе в withContext происходит "до" и при выходе из него происходит "до". - person Marko Topolnik; 17.06.2021
comment
Происходит ли распространение на всю память, даже на косвенно доступные члены членов объектов? - person Tenfour04; 17.06.2021
comment
@ Tenfour04 Да, так и происходит - прежде чем работает, это определяется не цепочкой доступа, а простой последовательностью действий. Все действия, выполняемые одним потоком, происходят друг перед другом в их программном порядке. Таким образом, поскольку withContext не вводит никакого параллелизма, все, что вам нужно, это два события «до ребра», которые переходят от начального потока-носителя к потоку внутри withContext, а затем обратно от него к начальному потоку. Но withContext здесь даже не особенный, каждый раз, когда функция приостанавливается, она может проснуться в другом потоке, и все просто предполагают, что это работает без проблем. - person Marko Topolnik; 17.06.2021
comment
@MarkoTopolnik Спасибо за объяснение! - person Tenfour04; 17.06.2021

Сопрограммы в Котлине действительно обеспечивают работу перед гарантиями.

Правило таково: внутри сопрограммы код до вызова функции приостановки происходит перед кодом после вызова приостановки.

Вы должны думать о сопрограммах, как если бы они были обычными потоками:

Несмотря на то, что сопрограмма в Kotlin может выполняться в нескольких потоках, с точки зрения изменяемого состояния она подобна потоку. Никакие два действия в одной и той же сопрограмме не могут выполняться одновременно.

Источник: https://proandroiddev.com/what-is-concurrent-access-to-mutable-state-f386e5cb8292

Возвращаясь к примеру кода. Захват переменных в телах лямбда-функций не идеален, особенно когда лямбда — это сопрограмма. Фактически,

Захват изменяемых переменных (var) в области действия такого блока почти всегда является ошибкой.

(заявление от КТ-15514)

Код перед лямбдой не предшествует коду внутри.

См. https://youtrack.jetbrains.com/issue/KT-15514.

person Sergei Voitovich    schedule 23.10.2019
comment
На самом деле правило таково: код перед вызовом функции приостановки происходит до кода внутри функции приостановки, происходит до кода после вызова приостановки. Это, в свою очередь, можно обобщить на порядок программы кода, который также является порядком кода происходит до. Обратите внимание на отсутствие в этом операторе чего-либо, относящегося к приостанавливаемым функциям. - person Marko Topolnik; 23.10.2019
comment
@Marko Tololnik Я не думаю, что это так, взгляните на youtrack.jetbrains.com /выпуск/KT-15514 - person Sergei Voitovich; 02.07.2020
comment
Я посмотрел, но не понял твоей мысли. В нем не обсуждается порядок вызовов приостанавливаемых функций. - person Marko Topolnik; 03.07.2020