Que signifie la fonction de suspension dans Kotlin Coroutine

118

Je lis Kotlin Coroutine et je sais qu'il est basé sur la suspendfonction. Mais qu'est-ce que ça suspendveut dire?

Coroutine ou fonction suspendue?

De https://kotlinlang.org/docs/reference/coroutines.html

Fondamentalement, les coroutines sont des calculs qui peuvent être suspendus sans bloquer un thread

J'ai souvent entendu les gens dire "suspendre la fonction". Mais je pense que c'est la coroutine qui est suspendue car elle attend que la fonction soit terminée? "suspendre" signifie généralement "arrêter l'opération", dans ce cas la coroutine est inactive.

🤔 Doit-on dire que la coroutine est suspendue?

Quelle coroutine est suspendue?

De https://kotlinlang.org/docs/reference/coroutines.html

Pour continuer l'analogie, await () peut être une fonction de suspension (donc également appelable depuis un bloc async {}) qui suspend une coroutine jusqu'à ce qu'un calcul soit fait et renvoie son résultat:

async { // Here I call it the outer async coroutine
    ...
    // Here I call computation the inner coroutine
    val result = computation.await()
    ...
}

🤔 Il dit "qui suspend une coroutine jusqu'à ce qu'un calcul soit fait", mais la coroutine est comme un thread léger. Donc, si la coroutine est suspendue, comment faire le calcul?

Nous voyons awaitest appelé computation, donc il se peut que asynccela retourne Deferred, ce qui signifie qu'il peut démarrer une autre coroutine

fun computation(): Deferred<Boolean> {
    return async {
        true
    }
}

🤔 La citation dit que suspend une coroutine . Cela signifie-t-il suspendla asynccoroutine externe ou suspendla computationcoroutine interne ?

Cela suspendsignifie que pendant que la asynccoroutine externe attend ( await) que la computationcoroutine interne se termine, elle (la asynccoroutine externe ) est inactive (d'où le nom suspend) et renvoie le thread au pool de threads, et lorsque la computationcoroutine enfant se termine, elle (la asynccoroutine externe) ) se réveille, prend un autre thread du pool et continue?

La raison pour laquelle je mentionne le fil est à cause de https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

Le thread est renvoyé dans le pool pendant que la coroutine est en attente, et lorsque l'attente est terminée, la coroutine reprend sur un thread libre dans le pool

onmyway133
la source

Réponses:

113

Suspendre les fonctions sont au centre de toutes les coroutines. Une fonction de suspension est simplement une fonction qui peut être mise en pause et reprise ultérieurement. Ils peuvent exécuter une opération de longue durée et attendre qu'elle se termine sans blocage.

La syntaxe d'une fonction de suspension est similaire à celle d'une fonction régulière à l'exception de l'ajout du suspendmot - clé. Il peut prendre un paramètre et avoir un type de retour. Cependant, les fonctions de suspension ne peuvent être appelées que par une autre fonction de suspension ou dans une coroutine.

suspend fun backgroundTask(param: Int): Int {
     // long running operation
}

Sous le capot, les fonctions de suspension sont converties par le compilateur en une autre fonction sans le mot clé suspend, qui prend un paramètre d'addition de type Continuation<T>. La fonction ci-dessus par exemple, sera convertie par le compilateur en ceci:

fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
   // long running operation
}

Continuation<T> est une interface qui contient deux fonctions appelées pour reprendre la coroutine avec une valeur de retour ou avec une exception si une erreur s'est produite lors de la suspension de la fonction.

interface Continuation<in T> {
   val context: CoroutineContext
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)
}
Sofien Rahmouni
la source
4
Un autre mystère dévoilé! Génial!
WindRider
16
Je me demande comment cette fonction est-elle réellement mise en pause? Ils disent toujours que cela suspend funpeut être mis en pause, mais comment exactement?
WindRider
2
@WindRider Cela signifie simplement que le thread actuel commence à exécuter une autre coroutine et y reviendra plus tard.
Joffrey
2
J'ai découvert le mécanisme "mystérieux". Il peut être facilement dévoilé à l'aide de Tools> Kotlin> Bytecode> Decompile btn. Il montre comment le soi-disant «point de suspension» est mis en œuvre - via Continuation et ainsi de suite. Tout le monde peut y jeter un coup d'œil.
WindRider
4
@buzaa Voici une conférence de 2017 de Roman Elizarov qui l'explique au niveau du bytecode.
Marko Topolnik le
30

Pour comprendre exactement ce que signifie suspendre une coroutine, je vous suggère de parcourir ce code:

import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

var continuation: Continuation<Int>? = null

fun main() = runBlocking {
    launch(Unconfined) {
        val a = a()
        println("Result is $a")
    }
    10.downTo(0).forEach {
        continuation!!.resume(it)
    }
}

suspend fun a(): Int {
    return b()
}

suspend fun b(): Int {
    while (true) {
        val i = suspendCoroutine<Int> { cont -> continuation = cont }
        if (i == 0) {
            return 0
        }
    }
}

Le Unconfinedrépartiteur de coroutine élimine la magie de la répartition de coroutine et nous permet de nous concentrer directement sur les coroutines nues.

Le code à l'intérieur du launchbloc commence à s'exécuter immédiatement sur le thread actuel, dans le cadre de l' launchappel. Voici ce qui se passe:

  1. Évaluer val a = a()
  2. Cela enchaîne b(), atteignant suspendCoroutine.
  3. La fonction b()exécute le bloc passé à suspendCoroutine, puis renvoie une COROUTINE_SUSPENDEDvaleur spéciale . Cette valeur n'est pas observable via le modèle de programmation Kotlin, mais c'est ce que fait la méthode Java compilée.
  4. Function a(), voyant cette valeur de retour, la renvoie également elle-même.
  5. Le launchbloc fait de même et le contrôle retourne maintenant à la ligne après l' launchinvocation:10.downTo(0)...

Notez qu'à ce stade, vous avez le même effet que si le code à l'intérieur du launchbloc et votre fun maincode s'exécutaient simultanément. Il se trouve que tout cela se passe sur un seul thread natif, de sorte que lelaunch bloc est "suspendu".

Maintenant, à l'intérieur du forEachcode en boucle, le programme lit le continuationque la b()fonction a écrit et resumesavec la valeur de 10. resume()est implémenté de telle manière que ce sera comme si l' suspendCoroutineappel renvoyé avec la valeur que vous avez transmise. Vous vous trouvez donc soudainement au milieu de l'exécution b(). La valeur que vous avez transmise resume()est attribuée iet vérifiée 0. Si ce n'est pas zéro, la while (true)boucle continue à l'intérieur b(), atteignant à nouveau suspendCoroutine, à quel point votre resume()appel revient, et maintenant vous passez par une autre étape de boucle forEach(). Cela continue jusqu'à ce que vous repreniez enfin avec 0, puis l' printlninstruction s'exécute et le programme se termine.

L'analyse ci-dessus devrait vous donner l'intuition importante que «suspendre une coroutine» signifie renvoyer le contrôle à l' launchinvocation la plus interne (ou, plus généralement, au constructeur de coroutine ). Si une coroutine se suspend à nouveau après la reprise, l' resume()appel se termine et le contrôle revient à l'appelant de resume().

La présence d'un répartiteur de coroutine rend ce raisonnement moins clair car la plupart d'entre eux soumettent immédiatement votre code à un autre thread. Dans ce cas, l'histoire ci-dessus se produit dans cet autre thread, et le répartiteur de coroutine gère également l' continuationobjet afin qu'il puisse le reprendre lorsque la valeur de retour est disponible.

Marko Topolnik
la source
21

Tout d'abord, la meilleure source pour comprendre cette OMI est le discours "Deep Dive into Coroutines" de Roman Elizarov.

Coroutine ou fonction suspendue?

L' appel d' une suspension ing fonction de suspension de la coroutine, qui signifie que le thread en cours peut commencer à exécuter une autre coroutine. Ainsi, on dit que la coroutine est suspendue plutôt que la fonction.

En fait, les sites d'appel des fonctions de suspension sont appelés «points de suspension» pour cette raison.

Quelle coroutine est suspendue?

Regardons votre code et décomposons ce qui se passe:

// 1. this call starts a new coroutine (let's call it C1).
//    If there were code after it, it would be executed concurrently with
//    the body of this async
async {
    ...
    // 2. this is a regular function call
    val deferred = computation()
    // 4. because await() is suspendING, it suspends coroutine C1.
    //    This means that if we had a single thread in our dispatcher, 
    //    it would now be free to go execute C2
    // 7. once C2 completes, C1 is resumed with the result `true` of C2's async
    val result = deferred.await() 
    ...
    // 8. C1 can now keep going in the current thread until it gets 
    //    suspended again (or not)
}

fun computation(): Deferred<Boolean> {
    // 3. this async call starts a second coroutine (C2). Depending on the 
    //    dispatcher you're using, you may have one or more threads.
    // 3.a. If you have multiple threads, the block of this async could be
    //      executed in parallel of C1 in another thread. The control flow 
    //      of the current thread returns to the caller of computation().
    // 3.b. If you have only one thread, the block is sort of "queued" but 
    //      not executed right away, and the control flow returns to the 
    //      caller of computation(). (unless a special dispatcher or 
    //      coroutine start argument is used, but let's keep it simple).
    //    In both cases, we say that this block executes "concurrently"
    //    with C1.
    return async {
        // 5. this may now be executed
        true
        // 6. C2 is now completed, so the thread can go back to executing 
        //    another coroutine (e.g. C1 here)
    }
}

L'extérieur asynccommence une coroutine. Lorsqu'il appelle computation(), l'interne asyncdémarre une seconde coroutine. Ensuite, l'appel à await()suspend l'exécution de la coroutine externe async , jusqu'à ce que l'exécution de la coroutine interne async soit terminée.

Vous pouvez même voir cela avec un seul thread: le thread exécutera le asyncdébut de l'extérieur , puis appellera computation()et atteindra l'intérieur async. À ce stade, le corps de l'asynchrone interne est ignoré et le thread continue d'exécuter l'externe asyncjusqu'à ce qu'il atteigne await(). await()est un "point de suspension", car awaitc'est une fonction de suspension. Cela signifie que la coroutine externe est suspendue, et ainsi le thread commence à exécuter la coroutine interne. Quand c'est fait, il revient pour exécuter la fin de l'extérieur async.

Suspend signifie-t-il que pendant que la coroutine asynchrone externe attend (attend) que la coroutine de calcul interne se termine, elle (la coroutine async externe) est inactive (d'où le nom suspend) et renvoie le thread au pool de threads, et lorsque la coroutine de calcul enfant se termine , il (la coroutine asynchrone externe) se réveille, prend un autre thread du pool et continue?

Oui, précisément.

La manière dont cela est réellement réalisé est de transformer chaque fonction de suspension en une machine à états, où chaque "état" correspond à un point de suspension à l'intérieur de cette fonction de suspension. Sous le capot, la fonction peut être appelée plusieurs fois, avec des informations sur le point de suspension à partir duquel elle doit commencer à s'exécuter (vous devriez vraiment regarder la vidéo que j'ai liée pour plus d'informations à ce sujet).

Joffrey
la source
3
Excellente réponse, ce genre d'explication vraiment basique me manque en ce qui concerne les coroutines.
bernardo.g
Pourquoi cela n'est-il pas mis en œuvre dans une autre langue? Ou est-ce que je manque quelque chose? Je pense à cette solution depuis si longtemps, heureux que Kotlin l'ait, mais je ne sais pas pourquoi TS ou Rust ont quelque chose comme ça
PEZO
Les coroutines @PEZO well existent depuis longtemps. Kotlin ne les a pas inventés, mais la syntaxe et la bibliothèque les font briller. Go a des goroutines, JavaScript et TypeScript ont des promesses. La seule différence réside dans les détails de la syntaxe pour les utiliser. Je trouve assez ennuyeux / dérangeant que les asyncfonctions de JS soient marquées de cette façon tout en retournant une promesse.
Joffrey le
Désolé, mon commentaire n'était pas clair. Je fais référence au mot-clé suspend. Ce n'est pas la même chose que async.
PEZO
Merci d'avoir indiqué la vidéo de Roman. Or pur.
Denounce'IN
8

J'ai trouvé que la meilleure façon de comprendre suspendest de faire une analogie entre thismot-clé et coroutineContextpropriété.

Les fonctions Kotlin peuvent être déclarées comme locales ou globales. Les fonctions locales ont comme par magie accès aux thismots-clés, contrairement aux fonctions globales.

Les fonctions Kotlin peuvent être déclarées comme suspendou bloquantes. suspendles fonctions ont par magie accès à la coroutineContextpropriété alors que les fonctions de blocage ne le font pas.

Le fait est que la coroutineContextpropriété est déclarée comme une propriété "normale" dans Kotlin stdlib mais cette déclaration n'est qu'un stub à des fins de documentation / navigation. En fait, coroutineContextc'est une propriété intrinsèque intégrée qui signifie que sous le capot, la magie du compilateur est consciente de cette propriété comme elle est consciente des mots-clés du langage.

Ce que fait le thismot-clé pour les fonctions locales, c'est ce que la coroutineContextpropriété fait pour les suspendfonctions: il donne accès au contexte actuel d'exécution.

Donc, vous devez suspendobtenir un accès à la coroutineContextpropriété - l'instance du contexte coroutine actuellement exécuté

Dmitry Kolesnikovich
la source
5

Je voulais vous donner un exemple simple du concept de continuation. C'est ce que fait une fonction de suspension, elle peut se figer / suspendre puis elle continue / reprend. Arrêtez de penser à la coroutine en termes de threads et de sémaphore. Pensez-y en termes de continuation et même de hooks de rappel.

Pour être clair, une coroutine peut être mise en pause à l'aide d'une suspendfonction. examinons ceci:

Dans Android, nous pourrions le faire par exemple:

var TAG = "myTAG:"
        fun myMethod() { // function A in image
            viewModelScope.launch(Dispatchers.Default) {
                for (i in 10..15) {
                    if (i == 10) { //on first iteration, we will completely FREEZE this coroutine (just for loop here gets 'suspended`)
                        println("$TAG im a tired coroutine - let someone else print the numbers async. i'll suspend until your done")
                        freezePleaseIAmDoingHeavyWork()
                    } else
                        println("$TAG $i")
                    }
            }

            //this area is not suspended, you can continue doing work
        }


        suspend fun freezePleaseIAmDoingHeavyWork() { // function B in image
            withContext(Dispatchers.Default) {
                async {
                    //pretend this is a big network call
                    for (i in 1..10) {
                        println("$TAG $i")
                        delay(1_000)//delay pauses coroutine, NOT the thread. use  Thread.sleep if you want to pause a thread. 
                    }
                    println("$TAG phwww finished printing those numbers async now im tired, thank you for freezing, you may resume")
                }
            }
        }

Le code ci-dessus imprime ce qui suit:

I: myTAG: my coroutine is frozen but i can carry on to do other things

I: myTAG: im a tired coroutine - let someone else print the numbers async. i'll suspend until your done

I: myTAG: 1
I: myTAG: 2
I: myTAG: 3
I: myTAG: 4
I: myTAG: 5
I: myTAG: 6
I: myTAG: 7
I: myTAG: 8
I: myTAG: 9
I: myTAG: 10

I: myTAG: phwww finished printing those numbers async now im tired, thank you for freezing, you may resume

I: myTAG: 11
I: myTAG: 12
I: myTAG: 13
I: myTAG: 14
I: myTAG: 15

imaginez que cela fonctionne comme ceci:

entrez la description de l'image ici

Ainsi, la fonction actuelle que vous avez lancée ne s'arrête pas, juste une coroutine serait suspendue pendant qu'elle continue. Le thread n'est pas interrompu par l'exécution d'une fonction de suspension.

Je pense que ce site peut vous aider et c'est ma référence.

Faisons quelque chose de cool et figeons notre fonction de suspension au milieu d'une itération. Nous le reprendrons plus tard dansonResume

Stockez une variable appelée continuationet nous la chargerons avec l'objet de continuation coroutines pour nous:

var continuation: CancellableContinuation<String>? = null

suspend fun freezeHere() = suspendCancellableCoroutine<String> {
            continuation = it
        }

 fun unFreeze() {
            continuation?.resume("im resuming") {}
        }

Maintenant, revenons à notre fonction suspendue et faisons-la geler au milieu de l'itération:

 suspend fun freezePleaseIAmDoingHeavyWork() {
        withContext(Dispatchers.Default) {
            async {
                //pretend this is a big network call
                for (i in 1..10) {
                    println("$TAG $i")
                    delay(1_000)
                    if(i == 3)
                        freezeHere() //dead pause, do not go any further
                }
            }
        }
    }

Puis ailleurs comme dans onResume (par exemple):

override fun onResume() {
        super.onResume()
        unFreeze()
    }

Et la boucle continuera. Il est assez intéressant de savoir que nous pouvons geler une fonction de suspension à tout moment et la reprendre après un certain temps. Vous pouvez également consulter les chaînes

j2emanue
la source
4

Comme de nombreuses bonnes réponses sont déjà là, je voudrais poster un exemple plus simple pour les autres.

Cas d' utilisation de runBlocking :

  • myMethod () est une suspendfonction
  • runBlocking { }démarre une Coroutine de manière bloquante. C'est similaire à la façon dont nous bloquions les threads normaux avec la Threadclasse et notifions les threads bloqués après certains événements.
  • runBlocking { }Does bloc le fil exécution en cours, jusqu'à ce que le coroutine (corps entre {}) se rempli

     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
        runBlocking {
            Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
            myMethod();
        }
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
    }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
        for(i in 1..5) {
            Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
        }
    }

Cela produit:

I/TAG: Outer code started on Thread : main
D/TAG: Inner code started  on Thread : main making outer code suspend
// ---- main thread blocked here, it will wait until coroutine gets completed ----
D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- main thread resumes as coroutine is completed ----
I/TAG: Outer code resumed on Thread : main

lancementcas d'utilisation de :

  • launch { } démarre une coroutine simultanément.
  • Cela signifie que lorsque nous spécifions launch, une coroutine démarre l'exécution sur le workerthread.
  • Le workerthread et le thread externe (à partir desquels nous avons appelé launch { }) s'exécutent tous deux simultanément. En interne, JVM peut effectuer threads préventifs
  • Lorsque nous avons besoin de plusieurs tâches pour s'exécuter en parallèle, nous pouvons l'utiliser. Il y en a scopesqui spécifient la durée de vie de la coroutine. Si nous spécifions GlobalScope, la coroutine fonctionnera jusqu'à la fin de la durée de vie de l'application.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
    
        GlobalScope.launch(Dispatchers.Default) {
            Log.d(TAG,"Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
            myMethod();
        }
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
    }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
            for(i in 1..5) {
                Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }

Ce produit:

10806-10806/com.example.viewmodelapp I/TAG: Outer code started on Thread : main
10806-10806/com.example.viewmodelapp I/TAG: Outer code resumed on Thread : main
// ---- In this example, main had only 2 lines to execute. So, worker thread logs start only after main thread logs complete
// ---- In some cases, where main has more work to do, the worker thread logs get overlap with main thread logs
10806-10858/com.example.viewmodelapp D/TAG: Inner code started  on Thread : DefaultDispatcher-worker-1 making outer code suspend
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-1

async et attendre le cas d'utilisation:

  • Lorsque nous avons plusieurs tâches à faire et qu'elles dépendent de l'achèvement des autres,async et awaitcela aiderait.
  • Par exemple, dans le code ci-dessous, il y a des 2fonctions de suspension myMethod () et myMethod2 (). myMethod2()devrait être exécuté seulement après l'achèvement complet de myMethod() OR myMethod2() dépend du résultat de myMethod(), nous pouvons utiliser asyncetawait
  • asyncdémarre une coroutine en parallèle similaire à launch. Mais, cela fournit un moyen d'attendre une coroutine avant de démarrer une autre coroutine en parallèle.
  • De cette façon await(). asyncrenvoie une instance de Deffered<T>. Tserait Unitpar défaut. Quand nous avons besoin d'attendre pour tout asyncl » achèvement, nous avons besoin de faire appel .await()à Deffered<T>instance de cette async. Comme dans l'exemple ci-dessous, nous avons appelé innerAsync.await()ce qui implique que l'exécution serait suspendue jusqu'à ce qu'elle innerAsyncsoit terminée. Nous pouvons observer la même chose en sortie. Le innerAsyncest terminé en premier, qui appelle myMethod(). Et puis async innerAsync2commence ensuite, qui appellemyMethod2()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
    
         job = GlobalScope.launch(Dispatchers.Default) {
             innerAsync = async {
                 Log.d(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
                 myMethod();
             }
             innerAsync.await()
    
             innerAsync2 = async {
                 Log.w(TAG, "Inner code started  on Thread : " + Thread.currentThread().name + " making outer code suspend");
                 myMethod2();
             }
        }
    
        Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
        }
    
    private suspend fun myMethod() {
        withContext(Dispatchers.Default) {
            for(i in 1..5) {
                Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }
    
    private suspend fun myMethod2() {
        withContext(Dispatchers.Default) {
            for(i in 1..10) {
                Log.w(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
            }
        }
    }

Cela produit:

11814-11814/? I/TAG: Outer code started on Thread : main
11814-11814/? I/TAG: Outer code resumed on Thread : main
11814-11845/? D/TAG: Inner code started  on Thread : DefaultDispatcher-worker-2 making outer code suspend
11814-11845/? D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- Due to await() call, innerAsync2 will start only after innerAsync gets completed
11814-11848/? W/TAG: Inner code started  on Thread : DefaultDispatcher-worker-4 making outer code suspend
11814-11848/? W/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 6 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 7 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 8 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 9 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 10 on Thread : DefaultDispatcher-worker-4
Kushal
la source