Kotlin сопрограммы «случается раньше» гарантирует?

10

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

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

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

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

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

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

private data class Data(var data: Int)
Василий
источник
Обратите внимание, что при работе на JVM Kotlin использует ту же модель памяти, что и Java.
Слоу
1
@ Слав, я это знаю. Однако под колпаком происходит много магии. Поэтому я хотел бы понять, есть ли какие-либо случаи, прежде чем гарантии, которые я получаю от сопрограмм, или это все от меня.
Василий
Во всяком случае, ваш второй пример представляет еще более простой сценарий: он просто использует объект, созданный внутри withContext, тогда как в первом примере он сначала создается, изменяется внутри withContext, а затем читает после withContext. Таким образом, в первом примере реализованы дополнительные функции безопасности потоков.
Марко Топольник
... и оба примера демонстрируют лишь один из аспектов "упорядочения программы", который встречается до, самый тривиальный. Я говорю об уровне сопрограмм здесь, а не об основной JVM. Итак, в основном, вы спрашиваете, настолько ли нарушены сопрограммы Kotlin, что они даже не обеспечивают порядок программ - раньше.
Марко Топольник
1
@MarkoTopolnik, поправьте меня, если я ошибаюсь, но JLS только гарантирует, что "программный порядок произойдет до" для выполнения в том же потоке. Теперь с сопрограммами, даже если код выглядит последовательным, на практике есть некоторый механизм, который выгружает его в разные потоки. Я понимаю вашу мысль: «Это такая основная гарантия, что я бы даже не потратил впустую свое время на ее проверку» (из другого комментария), но я задал этот вопрос, чтобы получить точный ответ. Я уверен, что примеры, которые я написал, безопасны для потока, но я хочу понять, почему.
Василий

Ответы:

6

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

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 могут выполняться в разных потоках. Поэтому ваш вопрос о гарантиях « до и после» в равной степени относится и к этому примеру. С другой стороны, в этом случае (я надеюсь) совершенно очевидно, что поведение этого кода соответствует принципам последовательного выполнения.

Марко Топольник
источник
1
Спасибо. Это на самом деле часть после "будь уверен", что побудило меня задать этот вопрос. Есть ли ссылки на документы, которые я мог прочитать? В качестве альтернативы, ссылки на исходный код, где это происходит до того, как установлено преимущество, также могут быть полезны (объединение, синхронизация или любой другой метод).
Василий
1
Это такая базовая гарантия, что я бы даже не стал тратить время на ее проверку. Под капотом все сводится к тому, executorService.submit()что есть какой-то типичный механизм ожидания завершения задания (завершение CompletableFutureили что-то подобное). С точки зрения котлинских сопрограмм здесь вообще нет параллелизма.
Марко Топольник
1
Вы можете думать о своем вопросе как о том, что вы спрашиваете: «Гарантирует ли ОС« что случится раньше »при приостановке потока и его возобновлении на другом ядре?». Потоки относятся к сопрограммам, а ядра ЦП - к потокам.
Марко Топольник
1
Спасибо за ваше объяснение. Однако я задал этот вопрос, чтобы понять, почему это работает. Я понимаю вашу точку зрения, но пока это не тот строгий ответ, который я ищу.
Василий
2
Ну ... на самом деле я не думаю, что этот поток установил, что код является последовательным. Это утверждал, конечно. Мне тоже было бы интересно увидеть механизм, который гарантирует, что пример будет работать так, как ожидается, без ущерба для производительности.
Г. Блейк Майке
3

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

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

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

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

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

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

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

Сергей Войтович
источник
На самом деле правило таково: код перед вызовом функции приостановки происходит до кода внутри функции приостановки, перед кодом после вызова приостановки. Это, в свою очередь, можно обобщить так: «программный порядок кода также является порядком, предшествующим коду ». Обратите внимание на отсутствие в этом выражении ничего специфического для приостановляемых функций.
Марко Топольник