Cosa significa funzione di sospensione in Kotlin Coroutine


118

Sto leggendo Kotlin Coroutine e so che si basa sulla suspendfunzione. Ma cosa vuol suspenddire?

La coroutine o la funzione vengono sospese?

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

Fondamentalmente, le coroutine sono calcoli che possono essere sospesi senza bloccare un thread

Ho sentito spesso la gente dire "sospendi la funzione". Ma penso che sia la coroutine che viene sospesa perché aspetta che la funzione finisca? "sospendere" di solito significa "cessare l'operazione", in questo caso la coroutine è inattiva.

🤔 Dovremmo dire che la coroutine è sospesa?

Quale coroutine viene sospesa?

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

Per continuare l'analogia, await () può essere una funzione di sospensione (quindi richiamabile anche dall'interno di un blocco {} asincrono) che sospende una coroutine fino a quando non viene eseguito un calcolo e restituisce il risultato:

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

🤔 Dice "che sospende una coroutine fino al completamento di alcuni calcoli", ma la coroutine è come un thread leggero. Quindi se la coroutine è sospesa, come può essere eseguito il calcolo?

Vediamo che awaitè chiamato computation, quindi potrebbe essere asyncche ritorni Deferred, il che significa che può avviare un'altra coroutine

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

🤔 La citazione dice che sospende una coroutine . Significa suspendla asynccoroutine esterna o suspendla computationcoroutine interna ?

Fa suspendche mentre esterna media asynccoroutine è in attesa ( await) per l'interno computationcoroutine alla fine, esso (l'esterno asynccoroutine) gira al minimo (da qui il nome di sospensione) e torna thread per il pool di thread, e quando il bambino computationfiniture coroutine, esso (l'esterno asynccoroutine ) si sveglia, prende un altro thread dal pool e continua?

Il motivo per cui cito il thread è a causa di https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

Il thread viene restituito al pool mentre la coroutine è in attesa, e quando l'attesa è terminata, la coroutine riprende su un thread libero nel pool

Risposte:


113

Le funzioni di sospensione sono al centro di tutte le coroutine. Una funzione di sospensione è semplicemente una funzione che può essere messa in pausa e ripresa in un secondo momento. Possono eseguire un'operazione di lunga durata e attendere che venga completata senza bloccarsi.

La sintassi di una funzione di sospensione è simile a quella di una funzione regolare tranne per l'aggiunta della suspendparola chiave. Può richiedere un parametro e avere un tipo restituito. Tuttavia, le funzioni di sospensione possono essere richiamate solo da un'altra funzione di sospensione o all'interno di una coroutine.

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

Dietro le quinte, le funzioni di sospensione vengono convertite dal compilatore in un'altra funzione senza la parola chiave suspend, che accetta un parametro aggiuntivo di tipo Continuation<T>. La funzione sopra, ad esempio, verrà convertita dal compilatore in questa:

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

Continuation<T> è un'interfaccia che contiene due funzioni che vengono invocate per riprendere la coroutine con un valore di ritorno o con un'eccezione se si è verificato un errore mentre la funzione era sospesa.

interface Continuation<in T> {
   val context: CoroutineContext
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)
}

4
Un altro mistero svelato! Grande!
WindRider

16
Mi chiedo come viene effettivamente messa in pausa questa funzione? Dicono sempre che suspend funpuò essere messo in pausa, ma come esattamente?
WindRider

2
@WindRider Significa solo che il thread corrente inizia a eseguire un'altra coroutine e tornerà su questa più tardi.
Joffrey

2
Ho scoperto il meccanismo "misterioso". Può essere facilmente svelato con l'aiuto di Strumenti> Kotlin> Bytecode> Decompile btn. Mostra come viene implementato il cosiddetto "punto di sospensione" - tramite Continuazione e così via. Tutti possono dare un'occhiata da soli.
WindRider

4
@buzaa Ecco un discorso del 2017 di Roman Elizarov che lo spiega a livello di bytecode.
Marko Topolnik,

30

Per capire cosa significa esattamente sospendere una coroutine, ti consiglio di seguire questo codice:

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

Il Unconfinedcoroutine dispatcher elimina la magia del coroutine dispatching e ci consente di concentrarci direttamente sulle coroutine nude.

Il codice all'interno del launchblocco inizia ad essere eseguito immediatamente sul thread corrente, come parte della launchchiamata. Quello che succede è il seguente:

  1. Valutare val a = a()
  2. Questo incatena b(), raggiungendo suspendCoroutine.
  3. La funzione b()esegue il blocco passato suspendCoroutinee poi restituisce un COROUTINE_SUSPENDEDvalore speciale . Questo valore non è osservabile attraverso il modello di programmazione Kotlin, ma è ciò che fa il metodo Java compilato.
  4. Anche la funzione a(), vedendo questo valore di ritorno, lo restituisce.
  5. Il launchblocco fa lo stesso e il controllo ora ritorna alla riga dopo l' launchinvocazione:10.downTo(0)...

Nota che, a questo punto, hai lo stesso effetto come se il codice all'interno del launchblocco e il tuo fun maincodice fossero eseguiti contemporaneamente. Succede solo che tutto questo avvenga su un singolo thread nativo quindi il launchblocco viene "sospeso".

Ora, all'interno del forEachcodice di loop, il programma legge il testo scritto continuationdalla b()funzione e resumeslo ha con il valore 10. resume()è implementato in modo tale che sarà come se la suspendCoroutinechiamata tornasse con il valore che hai passato. Quindi ti ritrovi improvvisamente nel mezzo dell'esecuzione b(). Il valore a cui hai passato resume()viene assegnato ie confrontato 0. Se non è zero, il while (true)ciclo continua all'interno b(), raggiungendo nuovamente suspendCoroutine, a quel punto la resume()chiamata ritorna e ora si esegue un altro passaggio di loop forEach(). Questo va avanti finché alla fine non si riprende con 0, quindi l' printlnistruzione viene eseguita e il programma viene completato.

L'analisi di cui sopra dovrebbe darvi l'importante intuizione che "sospendere una coroutine" significa riportare il controllo alla più intima launchinvocazione (o, più in generale, coroutine builder ). Se una coroutine si sospende di nuovo dopo la ripresa, la resume()chiamata termina e il controllo torna al chiamante di resume().

La presenza di un coroutine dispatcher rende questo ragionamento meno chiaro perché la maggior parte di loro invia immediatamente il codice a un altro thread. In quel caso la storia di cui sopra accade in quell'altro thread e il coroutine dispatcher gestisce anche l' continuationoggetto in modo che possa riprenderlo quando il valore restituito è disponibile.


21

Prima di tutto, la migliore fonte per capire questo IMO è il discorso "Deep Dive into Coroutines" di Roman Elizarov.

La coroutine o la funzione vengono sospese?

Chiamare una sospendere ing funzione di sospendere s il coroutine, significa che il thread corrente può iniziare l'esecuzione di un altro coroutine. Quindi, si dice che la coroutine sia sospesa piuttosto che la funzione.

Infatti, i siti di chiamata delle funzioni di sospensione sono chiamati "punti di sospensione" proprio per questo motivo.

Quale coroutine viene sospesa?

Diamo un'occhiata al tuo codice e analizziamo cosa succede:

// 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'esterno asyncinizia una coroutine. Quando chiama computation(), l'interno asyncavvia una seconda coroutine. Quindi, la chiamata a await()sospende l'esecuzione della coroutine esterna async , fino al termine dell'esecuzione della coroutine interna async .

Puoi anche vederlo con un singolo thread: il thread eseguirà l' asyncinizio dell'esterno, quindi chiamerà computation()e raggiungerà l'interno async. A questo punto, il corpo dell'asincronia interna viene ignorato e il thread continua a eseguire quello esterno asyncfinché non raggiunge await(). await()è un "punto di sospensione", perché awaitè una funzione di sospensione. Ciò significa che la coroutine esterna è sospesa e quindi il thread inizia a eseguire quella interna. Quando è finito, torna per eseguire la fine dell'esterno async.

Sospendere significa che mentre la coroutine asincrona esterna è in attesa (in attesa) che la coroutine di calcolo interna finisca, essa (la coroutine asincrona esterna) rimane inattiva (da cui il nome suspend) e restituisce il thread al pool di thread, e quando termina la coroutine di calcolo figlio , esso (la coroutine asincrona esterna) si sveglia, prende un altro thread dal pool e continua?

Sì, appunto.

Il modo in cui ciò viene effettivamente ottenuto è trasformare ogni funzione di sospensione in una macchina a stati, dove ogni "stato" corrisponde a un punto di sospensione all'interno di questa funzione di sospensione. Sotto il cofano, la funzione può essere chiamata più volte, con le informazioni su quale punto di sospensione dovrebbe iniziare l'esecuzione (dovresti davvero guardare il video che ho collegato per maggiori informazioni a riguardo).


3
Ottima risposta, mi manca quel tipo di spiegazione davvero basilare quando si tratta di coroutine.
bernardo.g

Perché non è implementato in un'altra lingua? Oppure mi sfugge qualcosa? Sto pensando a quella soluzione per così tanto tempo, felice che Kotlin ce l'abbia, ma non sono sicuro del motivo per cui TS o Rust hanno qualcosa del genere
PEZO

@PEZO well coroutines sono in circolazione da molto tempo. Kotlin non li ha inventati, ma la sintassi e la libreria li fanno brillare. Go ha goroutine, JavaScript e TypeScript hanno promesse. L'unica differenza è nei dettagli della sintassi per usarli. Trovo abbastanza fastidioso / fastidioso che le asyncfunzioni di JS siano contrassegnate in questo modo e tuttavia restituiscano una promessa.
Joffrey

Scusa, il mio commento non è stato chiaro. Mi riferisco alla parola chiave suspend. Non è la stessa cosa di async.
PEZO

Grazie per aver indicato il video di Roman. Oro zecchino.
Denuncia il

8

Ho scoperto che il modo migliore per capire suspendè fare un'analogia tra thisparola chiave e coroutineContextproprietà.

Le funzioni di Kotlin possono essere dichiarate come locali o globali. Le funzioni locali hanno magicamente accesso alla thisparola chiave mentre quelle globali no.

Le funzioni di Kotlin possono essere dichiarate come suspendo bloccanti. suspendle funzioni hanno magicamente accesso alla coroutineContextproprietà mentre le funzioni di blocco no.

Il fatto è: la coroutineContextproprietà è dichiarata come una proprietà "normale" in Kotlin stdlib ma questa dichiarazione è solo uno stub per scopi di documentazione / navigazione. In effetti coroutineContextè una proprietà intrinseca incorporata che significa che sotto il cofano la magia del compilatore è consapevole di questa proprietà come se fosse consapevole delle parole chiave del linguaggio.

Ciò che la thisparola chiave fa per le funzioni locali è ciò che la coroutineContextproprietà fa per le suspendfunzioni: dà accesso al contesto corrente di esecuzione.

Quindi, è necessario suspendottenere un accesso alla coroutineContextproprietà, l'istanza del contesto coroutine attualmente eseguito


5

Volevo darti un semplice esempio del concetto di continuazione. Questo è ciò che fa una funzione di sospensione, può congelare / sospendere e quindi continuare / riprendere. Smettila di pensare alla coroutine in termini di thread e semaforo. Pensa in termini di continuazione e persino di richiami.

Per essere chiari, una coroutine può essere messa in pausa utilizzando una suspendfunzione. esaminiamo questo:

In Android potremmo fare questo ad esempio:

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

Il codice precedente stampa quanto segue:

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

immagina che funzioni in questo modo:

inserisci qui la descrizione dell'immagine

Quindi la funzione corrente da cui hai avviato non si ferma, solo una coroutine si sospenderebbe mentre continua. Il thread non viene messo in pausa eseguendo una funzione di sospensione.

Penso che questo sito possa aiutarti a sistemare le cose ed è il mio riferimento.

Facciamo qualcosa di interessante e congeliamo la nostra funzione di sospensione nel mezzo di un'iterazione. Lo riprenderemo più tardionResume

Memorizza una variabile chiamata continuatione la caricheremo con l'oggetto di continuazione coroutines per noi:

var continuation: CancellableContinuation<String>? = null

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

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

Ora, torniamo alla nostra funzione sospesa e facciamo in modo che si blocchi nel mezzo dell'iterazione:

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

Quindi da qualche altra parte come in onResume (ad esempio):

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

E il ciclo continuerà. È abbastanza carino sapere che possiamo bloccare una funzione di sospensione in qualsiasi momento e riprenderla dopo che è trascorso un po 'di tempo. Puoi anche esaminare i canali


4

Poiché ci sono già molte buone risposte, vorrei postare un esempio più semplice per altri.

Caso d' uso runBlocking :

  • myMethod () è la suspendfunzione
  • runBlocking { }avvia una Coroutine in modo bloccante. È simile a come stavamo bloccando i thread normali con la Threadclasse e notificando i thread bloccati dopo determinati eventi.
  • runBlocking { }non bloccare la corrente di esecuzione filo, fino a quando il coroutine (corpo fra {}) viene completato

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

Questo produce:

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

caso d'uso di lancio :

  • launch { } avvia una coroutine contemporaneamente.
  • Ciò significa che quando specifichiamo il lancio, una coroutine inizia l'esecuzione su worker thread.
  • Il workerthread e il thread esterno (da cui abbiamo chiamato launch { }) vengono eseguiti contemporaneamente. Internamente, JVM può eseguire il threading preventivo
  • Quando abbiamo bisogno di più attività da eseguire in parallelo, possiamo usarlo. Ci sono scopesche specificano la durata della coroutine. Se specifichiamo GlobalScope, la coroutine funzionerà fino al termine della durata dell'applicazione.

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

Questo produce:

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

Caso d'uso asincrono e in attesa :

  • Quando abbiamo più attività da svolgere e dipendono dal completamento degli altri,async e awaitsarebbero utili.
  • Ad esempio, nel codice sottostante, ci sono 2le funzioni di sospensione myMethod () e myMethod2 (). myMethod2()dovrebbe essere eseguito solo dopo il completo completamento di myMethod() OR myMethod2() dipende dal risultato di myMethod(), possiamo usare asynceawait
  • async avvia una coroutine in parallelo simile a launch . Ma fornisce un modo per aspettare una coroutine prima di avviare un'altra coroutine in parallelo.
  • In questo modo è await(). asyncrestituisce un'istanza di Deffered<T>. Tsarebbe Unitper impostazione predefinita. Quando abbiamo bisogno di aspettare il completamento di qualsiasi cosa async, dobbiamo richiamare .await()su Deffered<T>istanza di quello async. Come nell'esempio seguente, abbiamo chiamato il innerAsync.await()che implica che l'esecuzione sarebbe stata sospesa fino al innerAsynccompletamento. Possiamo osservare lo stesso in output. La innerAsyncsi completa prima, che chiama myMethod(). E poi async innerAsync2inizia il prossimo , che chiamamyMethod2()

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

Questo produce:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.