Kotlin: withContext () vs Async-await


91

Kotlin belgelerini okuyordum ve doğru anladıysam iki Kotlin işlevi şu şekilde çalışıyor:

  1. withContext(context): verilen blok yürütüldüğünde, mevcut eşdizinin bağlamını değiştirir, eşgüdüm önceki bağlama geri döner.
  2. async(context): Verilen bağlamda yeni bir coroutine başlatır .await()ve döndürülen Deferredgörevi çağırırsak, çağıran koroutini askıya alır ve ortaya çıkan koroutin içinde çalıştırılan blok geri döndüğünde devam eder.

Şimdi aşağıdaki iki sürüm için code:

Versiyon 1:

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }

Versiyon 2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. Her iki sürümde de block1 (), block3 () varsayılan bağlamda (ortak havuz?) Çalıştırılır, burada block2 () verilen bağlamda çalışır.
  2. Genel yürütme, block1 () -> block2 () -> block3 () sırası ile eşzamanlıdır.
  3. Gördüğüm tek fark, sürüm1'in başka bir eşdüzey oluşturması, sürüm2'nin bağlam değiştirirken yalnızca bir eş yordam yürütmesi.

Sorularım:

  1. Kullanımı her zaman daha iyi değil midir withContextyerine async-awaito işlevsel benzer olarak, ancak başka eşyordam oluşturmaz. Çok sayıda koroutin, hafif olmasına rağmen, zorlu uygulamalarda yine de sorun olabilir.

  2. async-awaitDaha çok tercih edilen bir durum var mı withContext?

Güncelleme: Kotlin 1.2.50 artık dönüştürebileceği bir kod incelemesine sahip async(ctx) { }.await() to withContext(ctx) { }.


Bence kullandığınızda withContext, ne olursa olsun her zaman yeni bir koroutin oluşturulur. Kaynak kodundan görebildiğim bu.
stdout

@stdout async/awaitOP'ye göre yeni bir coroutine de yaratmıyor mu ?
IgorGanapolsky

Yanıtlar:


126

Çok sayıda koroutin, hafif olmasına rağmen, zorlu uygulamalarda yine de sorun olabilir

Gerçek maliyetlerini ölçerek bu "çok fazla eşgüdüm" sorununun bir sorun olduğu efsanesini ortadan kaldırmak istiyorum.

İlk olarak, koroutinin kendisini bağlı olduğu koroutin bağlamından ayırmalıyız . Minimum ek yük ile sadece bir korutin oluşturmanın yolu:

GlobalScope.launch(Dispatchers.Unconfined) {
    suspendCoroutine<Unit> {
        continuations.add(it)
    }
}

Bu ifadenin değeri Jobaskıya alınmış bir korutindir. Devamını korumak için daha geniş kapsamda bir listeye ekledik.

Bu kodu karşılaştırdım ve 140 bayt ayırdığı ve tamamlanması 100 nanosaniye sürdüğü sonucuna vardım . Demek bir koroutin bu kadar hafiftir.

Tekrarlanabilirlik için kullandığım kod bu:

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>>()

Bu kod, bir grup koroutini başlatır ve sonra uyur, böylece yığını VisualVM gibi bir izleme aracıyla analiz etmek için zamanınız olur. Özel sınıfları yarattım JobListve ContinuationListbu, yığın dökümünü analiz etmeyi kolaylaştırdığı için.


Daha kapsamlı bir şekilde elde etmek için, ben de maliyetini ölçmek için aşağıdaki kodu kullanılır withContext()ve async-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()
    }
}

Yukarıdaki koddan aldığım tipik çıktı bu:

Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds

Evet, async-awaitiki kat daha uzun sürer withContext, ancak yine de sadece bir mikrosaniye. Bunları sıkı bir döngü içinde başlatmanız ve bunun dışında neredeyse hiçbir şey yapmamanız gerekir, çünkü bunun uygulamanızda "sorun" haline gelmesi gerekir.

Kullanarak measureMemory()arama başına aşağıdaki bellek ücretini buldum:

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

Maliyeti , bir koroutinin hafıza ağırlığı olarak aldığımız sayıdan async-awaittam olarak 140 bayt daha yüksektir withContext. Bu, CommonPoolbağlamı kurmanın tam maliyetinin sadece bir kısmıdır .

Performans / bellek etkisi withContextve arasında karar vermek için tek kriter async-awaitolsaydı, sonuç, gerçek kullanım durumlarının% 99'unda aralarında anlamlı bir fark olmadığı olurdu.

Gerçek neden withContext(), özellikle istisna işleme açısından daha basit ve daha doğrudan bir API olmasıdır:

  • İçinde ele alınmayan bir istisna async { ... }, üst işinin iptal edilmesine neden olur. Bu, eşleşmedeki istisnaları nasıl ele aldığınıza bakılmaksızın gerçekleşir await(). coroutineScopeBunun için bir hazırlık yapmadıysanız, tüm başvurunuzu kapatabilir.
  • İçinde ele alınmayan bir istisna withContext { ... }, withContextçağrı tarafından atılır , siz onu tıpkı diğerleri gibi halledersiniz.

withContext ayrıca ebeveyn coroutine'i askıya aldığınız ve çocuğu beklediğiniz gerçeğinden yararlanarak optimize edilir, ancak bu sadece ek bir bonus.

async-awaitGerçekten eşzamanlılık istediğiniz durumlar için ayrılmalıdır, böylece arka planda birkaç koroutin başlatır ve ancak o zaman onları beklersiniz. Kısacası:

  • async-await-async-await - bunu yapma, kullan withContext-withContext
  • async-async-await-await - onu kullanmanın yolu bu.

Ekstra bellek maliyeti ile ilgili olarak async-await: Kullandığımızda withContext, yeni bir koroutin de yaratılır (kaynak kodundan görebildiğim kadarıyla), bu yüzden farkın başka bir yerden kaynaklanabileceğini düşünüyor musunuz?
stdout

1
@stdout Bu testleri çalıştırdığımdan beri kütüphane gelişiyor. Yanıttaki kodun tamamen bağımsız olması gerekiyor, doğrulamak için tekrar çalıştırmayı deneyin. bazı farklılıkları da açıklayabilecek asyncbir Deferrednesne yaratır .
Marko Topolnik

~ " Devamını korumak için ". Bunu ne zaman tutmamız gerekiyor?
IgorGanapolsky

1
@IgorGanapolsky Her zaman tutulur, ancak genellikle bir şekilde kullanıcı tarafından görülemez. Devamını kaybetmek, Thread.destroy()infazın havaya kaybolmasına eşdeğerdir .
Marko Topolnik

23

İşlevsel olarak benzer olduğu, ancak başka bir koroutin oluşturmadığı için asynch-await yerine withContext kullanmak her zaman daha iyi değil mi? Büyük sayılar coroutine, hafif olsa da zorlu uygulamalarda hala bir sorun olabilir

Asynch-await'in withContext'e göre daha çok tercih edildiği bir durum var mı?

Aynı anda birden çok görevi yürütmek istediğinizde async / await kullanmalısınız, örneğin:

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)
}

Aynı anda birden fazla görevi çalıştırmanız gerekmiyorsa withContext'i kullanabilirsiniz.


14

Şüphe duyduğunuzda, bunu pratik bir kural olarak hatırlayın:

  1. Birden fazla görevin paralel olarak gerçekleşmesi gerekiyorsa ve nihai sonuç hepsinin tamamlanmasına bağlıysa, kullanın async.

  2. Tek bir görevin sonucunu döndürmek için kullanın withContext.


1
Are hem asyncve withContextbir askıya kapsamında engelleme?
IgorGanapolsky

3
@IgorGanapolsky Eğer ana iş parçacığı engelleme bahsediyorsak, asyncve withContextbazı uzun çalışan görev çalışan ve sonuç beklerken ana iş parçacığı engellemez, sadece eşyordamın vücudu askıya alacaktır. Daha fazla bilgi ve örnek için Medium: Async Operations with Kotlin Coroutines hakkındaki bu makaleye bakın .
Yogesh Umesh Vaity
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.