O que significa suspender a função em Kotlin Coroutine


118

Estou lendo Kotlin Coroutine e sei que é baseado em suspendfunções. Mas o que isso suspendsignifica?

A co-rotina ou função é suspensa?

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

Basicamente, co-rotinas são cálculos que podem ser suspensos sem bloquear um thread

Já ouvi muitas pessoas dizerem "suspender função". Mas acho que é a co-rotina que fica suspensa porque está esperando a função terminar? "suspender" geralmente significa "cessar operação", neste caso a co-rotina está ociosa.

🤔 Devemos dizer que a co-rotina está suspensa?

Qual corrotina é suspensa?

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

Para continuar a analogia, await () pode ser uma função de suspensão (portanto, também pode ser chamada de dentro de um bloco {} assíncrono) que suspende uma co-rotina até que algum cálculo seja feito e retorne seu resultado:

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

🤔 Diz "que suspende uma co-rotina até que algum cálculo seja feito", mas a co-rotina é como uma thread leve. Portanto, se a co-rotina for suspensa, como o cálculo pode ser feito?

Vemos que awaité chamado computation, então pode ser asyncque retorne Deferred, o que significa que pode iniciar outra co-rotina

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

🤔 A citação diz que suspende uma co-rotina . Significa suspenda asyncco-rotina externa ou suspenda computationco-rotina interna ?

Faz suspendmédia que, enquanto exterior asyncco-rotina está esperando ( await) para o interior computationcoroutine ao fim, ele (o exterior asynccoroutine) idles (daí o nome suspensão) e retornos de rosca para o pool de threads, e quando a criança computationcoroutine acabamentos, ele (o exterior asynccoroutine ) acorda, pega outro thread do pool e continua?

Menciono o tópico por causa de https://kotlinlang.org/docs/tutorials/coroutines-basic-jvm.html

O encadeamento é retornado ao conjunto enquanto a co-rotina está esperando e quando a espera é concluída, a co-rotina é retomada em um encadeamento livre no conjunto

Respostas:


113

Funções suspensas estão no centro de todas as corrotinas. Uma função de suspensão é simplesmente uma função que pode ser pausada e retomada posteriormente. Eles podem executar uma operação de longa duração e esperar que ela seja concluída sem bloquear.

A sintaxe de uma função de suspensão é semelhante à de uma função regular, exceto pela adição da suspendpalavra - chave. Pode receber um parâmetro e ter um tipo de retorno. No entanto, as funções de suspensão só podem ser chamadas por outra função de suspensão ou dentro de uma co-rotina.

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

Por dentro, as funções de suspensão são convertidas pelo compilador em outra função sem a palavra-chave suspend, que recebe um parâmetro de adição do tipo Continuation<T>. A função acima, por exemplo, será convertida pelo compilador para esta:

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

Continuation<T> é uma interface que contém duas funções que são chamadas para retomar a co-rotina com um valor de retorno ou com uma exceção se um erro ocorreu enquanto a função estava suspensa.

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

4
Outro mistério desvendado! Ótimo!
— WindRider

16
Eu me pergunto como essa função é realmente pausada? Eles sempre dizem que suspend funpode ser pausado, mas como exatamente?
— WindRider

2
@WindRider Significa apenas que a thread atual começa a executar alguma outra co-rotina e voltará a esta mais tarde.
— Joffrey

2
Eu descobri o mecanismo "misterioso". Ele pode ser facilmente revelado com a ajuda de Ferramentas> Kotlin> Bytecode> Decompilar btn. Mostra como o chamado "ponto de suspensão" é implementado - via Continuação e assim por diante. Qualquer um pode dar uma olhada por si mesmo.
— WindRider

4
@buzaa Aqui está uma palestra de 2017 de Roman Elizarov que explica isso em nível de bytecode.
— Marko Topolnik

30

Para entender exatamente o que significa suspender uma co-rotina, sugiro que você leia este código:

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

O Unconfineddespachante de corrotina elimina a magia do despacho de corrotina e nos permite focar diretamente nas corrotinas vazias.

O código dentro do launchbloco começa a ser executado imediatamente no thread atual, como parte da launchchamada. O que acontece é o seguinte:

  1. Avalie val a = a()
  2. Este acorrenta a b(), alcançando suspendCoroutine.
  3. A função b()executa o bloco passado suspendCoroutinee retorna um COROUTINE_SUSPENDEDvalor especial . Esse valor não é observável por meio do modelo de programação Kotlin, mas é isso que o método Java compilado faz.
  4. A função a(), vendo esse valor de retorno, ela mesma também o retorna.
  5. O launchbloco faz o mesmo e o controle agora retorna para a linha após a launchinvocação:10.downTo(0)...

Observe que, neste ponto, você tem o mesmo efeito como se o código dentro do launchbloco e seu fun maincódigo estivessem sendo executados simultaneamente. Acontece que tudo isso está acontecendo em uma única thread nativa, então o launchbloco é "suspenso".

Agora, dentro do forEachcódigo de loop, o programa lê o continuationque a b()função escreveu e resumescom o valor de 10. resume()é implementado de tal forma que será como se a suspendCoroutinechamada retornasse com o valor que você passou. Então, de repente, você se encontra no meio da execução b(). O valor que você passou resume()é atribuído ie verificado 0. Se não for zero, o while (true)loop continua por dentro b(), alcançando novamente suspendCoroutine, ponto no qual sua resume()chamada retorna, e agora você passa por outra etapa de loop forEach(). Isso continua até que você finalmente reinicie com 0, então a printlninstrução é executada e o programa é concluído.

A análise acima deve dar a você a importante intuição de que "suspender uma co-rotina" significa retornar o controle de volta à launchinvocação mais interna (ou, mais geralmente, construtor de co-rotina ). Se uma co-rotina for suspensa novamente após retomar, a resume()chamada termina e o controle retorna para o chamador de resume().

A presença de um despachante de corrotina torna esse raciocínio menos claro porque a maioria deles envia imediatamente seu código para outro encadeamento. Nesse caso, a história acima acontece naquele outro encadeamento, e o distribuidor de co-rotina também gerencia o continuationobjeto para que possa retomá-lo quando o valor de retorno estiver disponível.


21

Em primeiro lugar, a melhor fonte para entender esta IMO é a palestra "Deep Dive into Coroutines" de Roman Elizarov.

A co-rotina ou função é suspensa?

Chamar um suspender ing função de suspensão s a co-rotina, ou seja, o segmento atual pode começar a executar um outro co-rotina. Portanto, a co - rotina é considerada suspensa em vez da função.

Na verdade, sites de chamadas de funções suspensas são chamados de "pontos de suspensão" por esse motivo.

Qual corrotina é suspensa?

Vamos analisar seu código e analisar o que acontece:

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

O externo asyncinicia uma co-rotina. Quando ele chama computation(), o interno asyncinicia uma segunda co-rotina. Então, a chamada para await()suspende a execução da co-rotina externa async , até que a execução da co-rotina interna async termine.

Você pode até ver isso com um único thread: o thread executará o asyncinício do externo , em seguida, chamará computation()e alcançará o interno async. Nesse ponto, o corpo do assíncrono interno é ignorado e o encadeamento continua executando o externo asyncaté atingir await(). await()é um "ponto de suspensão", porque awaité uma função de suspensão. Isso significa que a co-rotina externa é suspensa e, assim, a rosca começa a executar a interna. Quando estiver pronto, ele volta para executar o fim do externo async.

Suspender significa que enquanto a co-rotina assíncrona externa está esperando (aguarda) pela co-rotina de computação interna terminar, ela (a co-rotina assíncrona externa) fica inativa (daí o nome suspender) e retorna thread para o pool de threads, e quando a co-rotina de computação filha termina , ela (a co-rotina assíncrona externa) é ativada, pega outro encadeamento do pool e continua?

Sim, exatamente.

A maneira como isso é realmente alcançado é transformando cada função de suspensão em uma máquina de estado, onde cada "estado" corresponde a um ponto de suspensão dentro dessa função de suspensão. Sob o capô, a função pode ser chamada várias vezes, com as informações sobre o ponto de suspensão a partir do qual ela deve começar a ser executada (você realmente deve assistir ao vídeo vinculado para obter mais informações sobre isso).


3
Ótima resposta, sinto falta desse tipo de explicação realmente básica quando se trata de corrotinas.
— bernardo.g

Por que isso não está implementado em nenhuma outra linguagem? Ou eu estou esquecendo de alguma coisa? Estou pensando nessa solução há tanto tempo, fico feliz que Kotlin a tenha, mas não sei por que TS ou Rust têm algo assim
— PEZO

As corrotinas de poço @PEZO já existem há muito tempo. Kotlin não os inventou, mas a sintaxe e a biblioteca os fazem brilhar. Go tem goroutines, JavaScript e TypeScript têm promessas. A única diferença está nos detalhes da sintaxe para usá-los. Acho bastante irritante / perturbador para as asyncfunções de JS serem marcadas dessa forma e ainda assim retornar uma Promessa.
— Joffrey

Desculpe, meu comentário não foi claro. Refiro-me à palavra-chave suspender. Não é o mesmo que assíncrono.
— PEZO

Obrigado por apontar para o vídeo de Roman. Ouro puro.
— Denounce'IN

8

Descobri que a melhor maneira de entender suspendé fazer uma analogia entre thispalavra-chave e coroutineContextpropriedade.

As funções do Kotlin podem ser declaradas como locais ou globais. Funções locais magicamente têm acesso à thispalavra - chave, enquanto as globais não.

As funções do Kotlin podem ser declaradas como suspendou de bloqueio. suspendfunções magicamente têm acesso à coroutineContextpropriedade, enquanto funções de bloqueio não.

A questão é: a coroutineContextpropriedade é declarada como uma propriedade "normal" no Kotlin stdlib, mas esta declaração é apenas um esboço para fins de documentação / navegação. Na verdade, coroutineContexté uma propriedade intrínseca embutida, o que significa que a mágica do compilador está ciente dessa propriedade, assim como está ciente das palavras-chave da linguagem.

O que thispalavra-chave faz para funções locais é o que coroutineContextpropriedade faz para suspendfunções: ela dá acesso ao contexto atual de execução.

Então, você precisa suspendobter um acesso à coroutineContextpropriedade - a instância do contexto de co-rotina executado atualmente


5

Queria dar um exemplo simples do conceito de continuação. Isso é o que uma função de suspensão faz, ela pode congelar / suspender e então continuar / reiniciar. Pare de pensar na co-rotina em termos de threads e Semaphore. Pense nisso em termos de continuação e até mesmo ganchos de retorno de chamada.

Para ficar claro, uma co-rotina pode ser pausada usando uma suspendfunção. vamos investigar isso:

No android, podemos fazer isso, por exemplo:

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

O código acima imprime o seguinte:

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

imagine funcionando assim:

insira a descrição da imagem aqui

Portanto, a função atual a partir da qual você iniciou não para, apenas uma co-rotina seria suspensa enquanto continua. O thread não é pausado executando uma função de suspensão.

Acho que este site pode te ajudar a esclarecer as coisas e é minha referência.

Vamos fazer algo legal e congelar nossa função de suspensão no meio de uma iteração. Vamos retomar mais tarde emonResume

Armazene uma variável chamada continuatione vamos carregá-la com o objeto de continuação de co-rotinas para nós:

var continuation: CancellableContinuation<String>? = null

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

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

Agora, vamos retornar à nossa função suspensa e fazê-la congelar no meio da iteração:

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

Em seguida, em outro lugar, como onResume (por exemplo):

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

E o loop continuará. É muito legal saber que podemos congelar uma função de suspensão a qualquer momento e retomá-la após algum tempo. Você também pode procurar canais


4

Como já existem muitas respostas boas, gostaria de postar um exemplo mais simples para outros.

Caso de uso runBlocking :

  • myMethod () é suspendfunção
  • runBlocking { }inicia uma co-rotina em forma de bloqueio. É semelhante a como bloqueamos threads normais com Threadclasse e notificamos threads bloqueados após certos eventos.
  • runBlocking { }não bloquear a corrente de execução fio, até que a co-rotina (corpo entre {}) fica concluída

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

Isso resulta em:

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 de uso de lançamento :

  • launch { } inicia uma co-rotina simultaneamente.
  • Isso significa que quando especificamos launch, uma co-rotina começa a ser executada no workerthread.
  • O workerthread e o thread externo (a partir do qual chamamos launch { }) são executados simultaneamente. Internamente, a JVM pode executar Preemptive Threading
  • Quando precisamos que várias tarefas sejam executadas em paralelo, podemos usar isso. Existem scopesquais especificam o tempo de vida da co-rotina. Se especificarmos GlobalScope, a co-rotina funcionará até que o tempo de vida do aplicativo termine.

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

Isso resulta em:

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

assíncrono e aguardar caso de uso:

  • Quando temos várias tarefas a fazer e elas dependem da realização de outras, asynce awaitajudariam.
  • Por exemplo, no código a seguir, existem 2funções de suspensão myMethod () e myMethod2 (). myMethod2()deve ser executado somente após a conclusão completa de myMethod() OU myMethod2() depende do resultado de myMethod(), podemos usar asynceawait
  • asyncinicia uma co-rotina em paralelo semelhante a launch. Mas, ele fornece uma maneira de esperar por uma co-rotina antes de iniciar outra co-rotina em paralelo.
  • É assim await(). asyncretorna uma instância de Deffered<T>. Tseria Unitpor padrão. Quando precisamos de esperar por qualquer async's conclusão, precisamos chamada .await()na Deffered<T>instância dessa async. Como no exemplo abaixo, chamamos o innerAsync.await()que implica que a execução seria suspensa até innerAsyncser concluída. Podemos observar o mesmo na saída. O innerAsyncé concluído primeiro, que chama myMethod(). E então async innerAsync2começa a seguir , que chamamyMethod2()

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

Isso resulta em:

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.