Czytałem dokumentację Kotlin i jeśli dobrze zrozumiałem, dwie funkcje Kotlin działają w następujący sposób:
withContext(context)
: przełącza kontekst aktualnego programu, gdy wykonywany jest dany blok, program przełącza się z powrotem do poprzedniego kontekstu.async(context)
: Uruchamia nowy program w podanym kontekście i jeśli wywołasz.await()
zwróconeDeferred
zadanie, zawiesza on wywołujący program i wznawia działanie, gdy powróci blok wykonywany wewnątrz zrodzonego programu.
Teraz dla następujących dwóch wersji code
:
Wersja 1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
Wersja 2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- W obu wersjach block1 (), block3 () wykonuje się w domyślnym kontekście (commonpool?), Gdzie jako block2 () wykonuje się w danym kontekście.
- Cała realizacja jest synchroniczna z kolejnością blok1 () -> blok2 () -> blok3 ().
- Jedyną różnicą, jaką widzę, jest to, że version1 tworzy inny program, gdzie jako wersja2 wykonuje tylko jeden program podczas przełączania kontekstu.
Moje pytania to:
Czy nie zawsze lepiej jest używać
withContext
, niżasync-await
jest funkcjonalnie podobne, ale nie tworzy kolejnego programu. Duża liczba programów, choć lekka, może nadal stanowić problem w wymagających zastosowaniach.Czy
async-await
jest lepsza sprawa niżwithContext
?
Aktualizacja:
Kotlin 1.2.50 ma teraz inspekcję kodu, w której można go konwertować async(ctx) { }.await() to withContext(ctx) { }
.
kotlin
kotlin-coroutines
Mangat Rai Modi
źródło
źródło
withContext
, zawsze tworzony jest nowy program, niezależnie od tego. Oto, co widzę z kodu źródłowego.async/await
tworzy również nowego programu, zgodnie z OP?Odpowiedzi:
Chciałbym rozwiać ten mit, że „zbyt wiele programów” stanowi problem, określając ich faktyczny koszt.
Najpierw powinniśmy wyodrębnić sam program z kontekstu, do którego jest dołączony. W ten sposób tworzysz tylko coroutine z minimalnym narzutem:
GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> { continuations.add(it) } }
Wartością tego wyrażenia jest
Job
trzymanie zawieszonego programu. Aby zachować kontynuację, dodaliśmy go do listy w szerszym zakresie.Testowałem ten kod i doszedłem do wniosku, że przydziela 140 bajtów i trwa 100 nanosekund . A więc tak lekki jest coroutine.
Aby zapewnić powtarzalność, oto kod, którego użyłem:
fun measureMemoryOfLaunch() { val continuations = ContinuationList() val jobs = (1..10_000).mapTo(JobList()) { GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> { continuations.add(it) } } } (1..500).forEach { Thread.sleep(1000) println(it) } println(jobs.onEach { it.cancel() }.filter { it.isActive}) } class JobList : ArrayList<Job>() class ContinuationList : ArrayList<Continuation<Unit>>()
Ten kod uruchamia kilka programów, a następnie usypia, więc masz czas na analizę sterty za pomocą narzędzia do monitorowania, takiego jak VisualVM. Stworzyłem wyspecjalizowane klasy
JobList
iContinuationList
ponieważ ułatwia to analizę zrzutu sterty.Aby uzyskać pełniejszą historię, użyłem poniższego kodu, aby zmierzyć również koszt
withContext()
iasync-await
:import kotlinx.coroutines.* import java.util.concurrent.Executors import kotlin.coroutines.suspendCoroutine import kotlin.system.measureTimeMillis const val JOBS_PER_BATCH = 100_000 var blackHoleCount = 0 val threadPool = Executors.newSingleThreadExecutor()!! val ThreadPool = threadPool.asCoroutineDispatcher() fun main(args: Array<String>) { try { measure("just launch", justLaunch) measure("launch and withContext", launchAndWithContext) measure("launch and async", launchAndAsync) println("Black hole value: $blackHoleCount") } finally { threadPool.shutdown() } } fun measure(name: String, block: (Int) -> Job) { print("Measuring $name, warmup ") (1..1_000_000).forEach { block(it).cancel() } println("done.") System.gc() System.gc() val tookOnAverage = (1..20).map { _ -> System.gc() System.gc() var jobs: List<Job> = emptyList() measureTimeMillis { jobs = (1..JOBS_PER_BATCH).map(block) }.also { _ -> blackHoleCount += jobs.onEach { it.cancel() }.count() } }.average() println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds") } fun measureMemory(name:String, block: (Int) -> Job) { println(name) val jobs = (1..JOBS_PER_BATCH).map(block) (1..500).forEach { Thread.sleep(1000) println(it) } println(jobs.onEach { it.cancel() }.filter { it.isActive}) } val justLaunch: (i: Int) -> Job = { GlobalScope.launch(Dispatchers.Unconfined) { suspendCoroutine<Unit> {} } } val launchAndWithContext: (i: Int) -> Job = { GlobalScope.launch(Dispatchers.Unconfined) { withContext(ThreadPool) { suspendCoroutine<Unit> {} } } } val launchAndAsync: (i: Int) -> Job = { GlobalScope.launch(Dispatchers.Unconfined) { async(ThreadPool) { suspendCoroutine<Unit> {} }.await() } }
Oto typowe dane wyjściowe, które otrzymuję z powyższego kodu:
Just launch: 140 nanoseconds launch and withContext : 520 nanoseconds launch and async-await: 1100 nanoseconds
Tak,
async-await
trwa około dwa razy dłużejwithContext
, ale to wciąż tylko mikrosekunda. Trzeba by było uruchamiać je w ciasnej pętli, prawie nic poza tym, żeby stało się to „problemem” w Twojej aplikacji.Używając
measureMemory()
znalazłem następujący koszt pamięci na połączenie:Just launch: 88 bytes withContext(): 512 bytes async-await: 652 bytes
Koszt
async-await
jest dokładnie 140 bajtów wyższy niżwithContext
liczba, którą otrzymaliśmy jako wagę pamięci jednego programu. To tylko ułamek całkowitego kosztu konfiguracjiCommonPool
kontekstu.Gdyby wpływ na wydajność / pamięć był jedynym kryterium wyboru między
withContext
aasync-await
, wniosek musiałby być taki, że nie ma między nimi istotnej różnicy w 99% rzeczywistych przypadków użycia.Prawdziwym powodem jest to, że
withContext()
prostszy i bardziej bezpośredni interfejs API, szczególnie pod względem obsługi wyjątków:async { ... }
powoduje anulowanie jego zadania nadrzędnego. Dzieje się tak niezależnie od tego, jak obsłużysz wyjątki z dopasowywaniaawait()
. Jeśli nie przygotowałeścoroutineScope
do tego celu, może to spowodować uszkodzenie całej aplikacji.withContext { ... }
zostaje po prostu wyrzucony przezwithContext
wywołanie, a Ty obsługujesz go tak jak każdy inny.withContext
zdarza się również, że jest zoptymalizowany, wykorzystując fakt, że zawieszasz program rodzica i czekasz na dziecko, ale to tylko dodatkowa premia.async-await
powinien być zarezerwowany dla tych przypadków, w których faktycznie chcesz współbieżności, aby uruchomić kilka programów w tle i dopiero potem na nie czekać. W skrócie:async-await-async-await
- nie rób tego, użyjwithContext-withContext
async-async-await-await
- w ten sposób można to wykorzystać.źródło
async-await
: Kiedy używamywithContext
, tworzony jest również nowy program (o ile widzę z kodu źródłowego), więc czy uważasz, że różnica może pochodzić z innego miejsca?async
tworzyDeferred
obiekt, który może również wyjaśniać tę różnicę.Thread.destroy()
- wykonaniem rozpływającym się w powietrzu.Powinieneś używać async / await, gdy chcesz wykonywać wiele zadań jednocześnie, na przykład:
runBlocking { val deferredResults = arrayListOf<Deferred<String>>() deferredResults += async { delay(1, TimeUnit.SECONDS) "1" } deferredResults += async { delay(1, TimeUnit.SECONDS) "2" } deferredResults += async { delay(1, TimeUnit.SECONDS) "3" } //wait for all results (at this point tasks are running) val results = deferredResults.map { it.await() } println(results) }
Jeśli nie musisz jednocześnie uruchamiać wielu zadań, możesz użyć funkcji withContext.
źródło
W razie wątpliwości pamiętaj o następującej zasadzie:
Jeśli wiele zadań musi być wykonywanych równolegle, a ostateczny wynik zależy od ukończenia wszystkich z nich, użyj
async
.Aby zwrócić wynik pojedynczego zadania, użyj
withContext
.źródło
async
iwithContext
blokowanie są w zawieszonym zakresie?async
iwithContext
nie będziesz blokować głównego wątku, będą zawieszać treść programu tylko wtedy, gdy jakieś długotrwałe zadanie jest uruchomione i czeka na wynik. Aby uzyskać więcej informacji i zapoznać się z przykładem, zobacz ten artykuł na Medium: Async Operations with Kotlin Coroutines .