Gwarancje Kotlin „zdarzają się wcześniej”

10

Czy karliny Kotlin dają jakieś gwarancje „zdarzy się przed”?

Na przykład, czy w tym przypadku istnieje gwarancja „dzieje się przed” między zapisem mutableVara późniejszym odczytem (potencjalnie) innego wątku:

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

Edytować:

Być może dodatkowy przykład lepiej wyjaśni pytanie, ponieważ jest to bardziej Kotlin (z wyjątkiem zmienności). Czy ten kod jest bezpieczny dla wątków:

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

private data class Data(var data: Int)
Wasilij
źródło
Należy pamiętać, że podczas uruchamiania na JVM Kotlin używa tego samego modelu pamięci co Java.
Sław
1
@Slaw, wiem o tym. Jednak pod maską dzieje się dużo magii. Dlatego chciałbym zrozumieć, czy zdarzają się jakieś gwarancje, które otrzymuję od coroutines, czy to wszystko na mnie.
Vasiliy
Jeśli już, to twój drugi przykład przedstawia jeszcze prostszy scenariusz: po prostu używa obiektu utworzonego w withContextśrodku, podczas gdy pierwszy przykład tworzy go najpierw, mutuje wewnątrz withContext, a następnie czyta później withContext. Tak więc pierwszy przykład zawiera więcej funkcji bezpieczeństwa wątków.
Marko Topolnik
... i oba przykłady wykonują tylko aspekt „kolejności programów” wcześniejszego, najbardziej trywialnego. Mówię tutaj o poziomie korupcji, a nie o JVM. Zasadniczo pytasz, czy kortyny Kotlina są tak poważnie rozbite, że nawet nie zapewniają, aby kolejność programów się zdarzyła wcześniej.
Marko Topolnik
1
@MarkoTopolnik, popraw mnie, jeśli się mylę, ale JLS gwarantuje tylko „zamówienie programu dzieje się przed” dla wykonania w tym samym wątku. Teraz, jeśli chodzi o coroutines, mimo że kod wygląda sekwencyjnie, w praktyce istnieje pewna maszyneria, która przenosi go do różnych wątków. Rozumiem twoją tezę: „to taka podstawowa gwarancja, że ​​nawet nie marnowałbym czasu na jej sprawdzanie” (z innego komentarza), ale zadałem to pytanie, aby uzyskać rygorystyczną odpowiedź. Jestem pewien, że przykłady, które napisałem, są bezpieczne dla wątków, ale chcę zrozumieć, dlaczego.
Wasilij

Odpowiedzi:

6

Napisany kod ma trzy dostępy do stanu współdzielonego:

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

Trzy dostępy są ściśle uporządkowane kolejno, bez współbieżności między nimi, i można mieć pewność, że infrastruktura Kotlin za dba ustanowienia dzieje, przed krawędzią gdy przekazanie off do IOpuli wątków i powrotem do wywołującego współprogram.

Oto równoważny przykład, który może być bardziej przekonujący:

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

Ponieważ delayjest to funkcja zawieszalna i ponieważ używamy programu Defaultrozsyłającego, który jest wspierany przez pulę wątków, linie 1, 2 i 3 mogą być wykonywane w innym wątku. Dlatego twoje pytanie dotyczące gwarancji przed zdarzeniem dotyczy w równym stopniu tego przykładu. Z drugiej strony w tym przypadku (mam nadzieję) jest całkowicie oczywiste, że zachowanie tego kodu jest zgodne z zasadami sekwencyjnego wykonywania.

Marko Topolnik
źródło
1
Dzięki. Właściwie to ta część po „pewności” zmotywowała mnie do zadania tego pytania. Czy są jakieś linki do dokumentów, które mógłbym przeczytać? Alternatywnie, pomocne mogą być również linki do kodu źródłowego, w którym tak się dzieje - zanim zostanie ustalone zbocze (łączenie, synchronizacja lub dowolna inna metoda).
Wasilij
1
Jest to tak podstawowa gwarancja, że ​​nawet nie marnowałbym czasu na jej sprawdzanie. Pod maską sprowadza się do executorService.submit()i jest pewien typowy mechanizm oczekiwania na zakończenie zadania (wykonanie CompletableFutureczegoś podobnego). Z punktu widzenia Kotlinów nie ma tu żadnej współbieżności.
Marko Topolnik
1
Możesz pomyśleć o swoim pytaniu jako analogicznym do pytania „czy system operacyjny gwarantuje, że zdarzy się to wcześniej niż podczas zawieszania wątku, a następnie wznawiania go na innym rdzeniu?” Wątki mają na celu określenie, jakie rdzenie procesora są dla wątków.
Marko Topolnik
1
Dziękuję za wyjaśnienie. Zadałem to pytanie, aby zrozumieć, dlaczego to działa. Rozumiem twój punkt widzenia, ale jak dotąd nie jest to rygorystyczna odpowiedź, której szukam.
Wasilij
2
Cóż ... Właściwie nie sądzę, aby ten wątek ustalił, że kod jest sekwencyjny. Z pewnością to potwierdził. Ja również chciałbym zobaczyć mechanizm, który gwarantuje, że przykład zachowuje się zgodnie z oczekiwaniami, bez wpływu na wydajność.
G. Blake Meike
3

Coroutines in Kotlin naprawdę się dzieje przed gwarancjami.

Zasada jest następująca: wewnątrz coroutine kod przed wywołaniem funkcji zawieszenia występuje przed kodem po wywołaniu zawieszenia.

Powinieneś pomyśleć o coroutines tak, jakby były zwykłymi wątkami:

Mimo że koruta w Kotlinie może wykonywać wiele wątków, jest ona jak wątek z punktu widzenia stanu zmiennego. Żadne dwie akcje w tym samym języku nie mogą być równoległe.

Źródło: https://proandroiddev.com/what-is-concurrent-access-to-mutable-state-f386e5cb8292

Wracając do przykładu kodu. Wychwytywanie zmiennych w ciałach funkcji lambda nie jest najlepszym pomysłem, szczególnie gdy lambda jest koroutyną. Kod przed lambda nie występuje przed kodem w środku.

Zobacz https://youtrack.jetbrains.com/issue/KT-15514

Siergiej Voitovich
źródło
Zasada jest taka: w rzeczywistości: kod przed wywołaniem funkcji zawieszenia ma miejsce - przed kodem wewnątrz funkcji zawieszenia, dzieje się - przed kodem po wywołaniu zawieszenia. To z kolei można uogólnić w taki sposób, że „kolejność programu w kodzie jest również kolejnością kodu przed kolejnością”. Zwróć uwagę na brak w tym stwierdzeniu czegoś specyficznego dla funkcji do zawieszenia.
Marko Topolnik