GlobalScope vs CoroutineScope vs lifecycleScope
Estou habituado a trabalhar AsyncTask
e a compreender muito bem devido à sua simplicidade. Mas Coroutines
são confusos para mim. Você pode me explicar de uma forma simples qual é a diferença e o propósito de cada um dos itens a seguir?
GlobalScope.launch(Dispatchers.IO) {}
GlobalScope.launch{}
CoroutineScope(Dispatchers.IO).launch{}
lifecycleScope.launch(Dispatchers.IO){}
lifecycleScope.launch{}
Respostas
Primeiro, vamos começar com definições para deixar isso claro. Se você precisar de um tutorial ou playground para Corrotinas e Fluxo de Corrotinas, você pode conferir este tutorial / playground que criei.
Scope
é o objeto que você usa para lançar corrotinas que contém apenas um objeto que é CoroutineContext
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
O contexto da co-rotina é um conjunto de regras e configurações que definem como a co-rotina será executada. Nos bastidores, é uma espécie de mapa, com um conjunto de chaves e valores possíveis.
O contexto de co-rotina é imutável, mas você pode adicionar elementos a um contexto usando o operador mais, assim como adiciona elementos a um conjunto, produzindo uma nova instância de contexto
O conjunto de elementos que definem o comportamento de uma co-rotina são:
- CoroutineDispatcher - despacha o trabalho para a thread apropriada.
- Trabalho - controla o ciclo de vida da co-rotina.
- CoroutineName - nome da co-rotina, útil para depuração.
- CoroutineExceptionHandler - lida com exceções não capturadas
Dispatchers Os distribuidores determinam qual thread pool deve ser usado. A classe Dispatchers também é CoroutineContext, que pode ser adicionada a CoroutineContext
Dispatchers.Default : Trabalho que consome muita CPU, como classificar grandes listas, fazer cálculos complexos e similares. Um conjunto compartilhado de encadeamentos na JVM o apóia.
Dispatchers.IO : rede ou leitura e gravação de arquivos. Resumindo - qualquer entrada e saída, como o nome indica
Dispatchers.Main : despachante obrigatório para realizar eventos relacionados à IU no thread principal ou IU do Android.
Por exemplo, mostrar listas em um RecyclerView, atualizar Views e assim por diante.
Você pode verificar os documentos oficiais do Android para obter mais informações sobre despachantes.
Editar Embora o documento oficial afirme queDispatchers.IO - Este dispatcher é otimizado para executar E / S de disco ou rede fora do encadeamento principal. Os exemplos incluem o uso do componente Room, leitura ou gravação de arquivos e execução de quaisquer operações de rede.
Resposta de Marko Topolnic
IO executa a co-rotina em um pool de threads flexível especial. Ele existe apenas como uma solução alternativa quando você é forçado a usar uma API de IO de bloqueio herdada que bloquearia seu thread de chamada.
pode estar certo também.
Job A própria co-rotina é representada por um Job. Um trabalho é um identificador para uma co-rotina. Para cada corrotina que você cria (por inicialização ou assíncrona), ele retorna uma instância de Job que identifica exclusivamente a corrotina e gerencia seu ciclo de vida. Você também pode passar um trabalho para um CoroutineScope para controlar seu ciclo de vida.
É responsável pelo ciclo de vida da co-rotina, cancelamento e relações pai-filho. Um trabalho atual pode ser recuperado de um contexto de co-rotina atual: Um trabalho pode passar por um conjunto de estados: Novo, Ativo, Concluindo, Concluído, Cancelando e Cancelado. embora não tenhamos acesso aos próprios estados, podemos acessar as propriedades de um Job: isActive, isCancelled e isCompleted.
CoroutineScope É definida uma função de fábrica simples que leva CoroutineContext
s como argumentos para criar wrapper em torno do CoroutineContext combinado como
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
internal class ContextScope(context: CoroutineContext) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
// CoroutineScope is used intentionally for user-friendly representation
override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}
e cria um Job
elemento se o contexto fornecido ainda não tiver um.
Vejamos o código-fonte GlobalScope
/**
* A global [CoroutineScope] not bound to any job.
*
* Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
* and are not cancelled prematurely.
* Another use of the global scope is operators running in [Dispatchers.Unconfined], which don't have any job associated with them.
*
* Application code usually should use an application-defined [CoroutineScope]. Using
* [async][CoroutineScope.async] or [launch][CoroutineScope.launch]
* on the instance of [GlobalScope] is highly discouraged.
*
* Usage of this interface may look like this:
*
* ```
* fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = GlobalScope.produce(Dispatchers.Unconfined) {
* for (number in this) {
* send(Math.sqrt(number))
* }
* }
* ```
*/
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
Como você pode ver, estende-se CoroutineScope
1- O GlobalScope está ativo enquanto seu aplicativo estiver ativo, se você fizer alguma contagem, por exemplo, neste escopo e girar seu dispositivo, ele continuará a tarefa / processo.
GlobalScope.launch(Dispatchers.IO) {}
é executado enquanto seu aplicativo está ativo, mas em thread de IO devido ao uso Dispatchers.IO
2- É o mesmo que o primeiro, mas por padrão, se você não tiver nenhum contexto, o launch usa EmptyCoroutineContext que usa Dispatchers.Default, então a única diferença é thread com o primeiro.
3- Este é igual ao primeiro com apenas diferença de sintaxe.
4- lifecycleScope
é uma extensão para LifeCycleOwner
e ligada ao ciclo de vida de Actvity ou Fragment, onde o escopo é cancelado quando aquela Activity ou Fragment é destruída.
/**
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
*/
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
Você também pode usar isso como
class Activity3CoroutineLifecycle : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main + CoroutineName("🙄 Activity Scope") + CoroutineExceptionHandler { coroutineContext, throwable ->
println("🤬 Exception $throwable in context:$coroutineContext") } private val dataBinding by lazy { Activity3CoroutineLifecycleBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(dataBinding.root) job = Job() dataBinding. button.setOnClickListener { // This scope lives as long as Application is alive GlobalScope.launch { for (i in 0..300) { println("🤪 Global Progress: $i in thread: ${Thread.currentThread().name}, scope: $this")
delay(300)
}
}
// This scope is canceled whenever this Activity's onDestroy method is called
launch {
for (i in 0..300) {
println("😍 Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this") withContext(Dispatchers.Main) { dataBinding.tvResult.text = "😍 Activity Scope Progress: $i in thread: ${Thread.currentThread().name}, scope: $this"
}
delay(300)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
Eu organizaria sua lista em três eixos:
GlobalScope
vs.CoroutineScope()
vs.lifecycleScope
Dispatchers.IO
vs. expedidor herdado (implícito)- Especifique o despachante no escopo vs. como um argumento para
launch
1. Escolha do escopo
Uma grande parte da visão de Kotlin sobre corrotinas é a simultaneidade estruturada , o que significa que todas as corrotinas são organizadas em uma hierarquia que segue suas dependências. Se você estiver iniciando algum trabalho em segundo plano, presumimos que você espera que seus resultados apareçam em algum ponto enquanto a "unidade de trabalho" atual ainda está ativa, ou seja, o usuário não saiu dela e não se importa mais seu resultado.
No Android, você tem lifecycleScope
à sua disposição o que segue automaticamente a navegação do usuário nas atividades da IU, então você deve usá-lo como o pai do trabalho em segundo plano cujos resultados ficarão visíveis para o usuário.
Você também pode ter algum trabalho dispare e esqueça, que você só precisa terminar eventualmente, mas o usuário não espera o resultado. Para isso, você deve usar o Android WorkManager
ou recursos semelhantes que podem continuar com segurança, mesmo se o usuário mudar para outro aplicativo. Geralmente, são tarefas que sincronizam seu estado local com o estado mantido no lado do servidor.
Nesta imagem, GlobalScope
é basicamente uma saída de emergência da simultaneidade estruturada. Ele permite que você satisfaça a forma de fornecer um escopo, mas anula todos os mecanismos que ele deve implementar. GlobalScope
nunca pode ser cancelado e não tem pai.
Escrever CoroutineScope(...).launch
é simplesmente errado porque você cria um objeto de escopo sem um pai que você imediatamente esquece e, portanto, não tem como cancelá-lo. É semelhante ao uso, GlobalScope
mas ainda mais hacky.
2. Escolha do expedidor
O despachante da co-rotina decide em quais threads sua co-rotina pode rodar. No Android, existem três despachantes com os quais você deve se preocupar:
Main
executa tudo em um único thread da GUI. Deve ser sua escolha principal.IO
executa a co-rotina em um pool de threads flexível especial. Ele existe apenas como uma solução alternativa quando você é forçado a usar uma API de IO de bloqueio herdada que bloquearia seu thread de chamada.Default
também usa um pool de threads, mas de tamanho fixo, igual ao número de núcleos da CPU. Use-o para trabalho de computação intensiva que levaria tempo o suficiente para causar uma falha na GUI (por exemplo, compressão / descompressão de imagem).
3. Onde especificar o expedidor
Primeiro, você deve estar ciente do dispatcher especificado no escopo da co-rotina que está usando. GlobalScope
não especifica nenhum, então o padrão geral está em vigor, o Default
despachante. lifecycleScope
especifica o Main
despachante.
Já explicamos que você não deve criar escopos ad-hoc usando o CoroutineScope
construtor, portanto, o local adequado para especificar um despachante explícito é como um parâmetro para launch
.
Em detalhes técnicos, quando você escreve someScope.launch(someDispatcher)
, o someDispatcher
argumento é na verdade um objeto de contexto de co-rotina completo que por acaso tem um único elemento, o despachante. A co-rotina que você está iniciando cria um novo contexto para si mesma, combinando aquele no escopo da co-rotina e aquele que você fornece como parâmetro. Além disso, ele cria um novo Job
para si mesmo e o adiciona ao contexto. O trabalho é filho daquele que foi herdado no contexto.
TL; DR
GlobalScope.launch (Dispatchers.IO) : Lança uma corrotina de nível superior em
Dispatchers.IO
. A corrotina é desassociada e continua em execução até ser concluída ou cancelada. Freqüentemente desanimado, pois o programador precisa manter uma referência parajoin()
oucancel()
.GlobalScope.launch : O mesmo que acima, mas
GlobalScope
usaDispatchers.Default
se não for especificado. Muitas vezes desanimado.CoroutineScope (Dispatchers.IO) .launch : Cria um escopo de co-rotina que usa, a
Dispatchers.IO
menos que um despachante seja especificado no construtor de co-rotina, ou sejalaunch
CoroutineScope (Dispatchers.IO) .launch (Dispatchers.Main) : Bônus um. Usa o mesmo escopo de co-rotina acima (se a instância de escopo for a mesma!), Mas substitui
Dispatcher.IO
porDispatchers.Main
para esta co-rotina.lifecycleScope.launch (Dispatchers.IO) : lança uma corrotina dentro do lifecycleScope fornecido pelo AndroidX. A corrotina é cancelada assim que o ciclo de vida é invalidado (isto é, o usuário sai de um fragmento). Usado
Dispatchers.IO
como pool de threads.lifecycleScope.launch : O mesmo que acima, mas usa
Dispatchers.Main
se não for especificado.
Explantion
O escopo da corrotina promove simultaneidade estruturada , por meio da qual você pode iniciar várias corrotinas no mesmo escopo e cancelar o escopo (o que, por sua vez, cancela todas as corrotinas dentro desse escopo) se for necessário. Pelo contrário, uma co-rotina GlobalScope é semelhante a um fio, onde você precisa manter uma referência no fim de join()
ou cancel()
-lo. Aqui está um excelente artigo de Roman Elizarov no Medium .
CoroutineDispatcher informa ao construtor de co-rotina (em nosso caso launch {}
) qual pool de threads deve ser usado. Existem alguns Dispatchers predefinidos disponíveis.
Dispatchers.Default
- Usa um pool de threads equivalente ao número de núcleos da CPU. Deve ser usado para carga de trabalho vinculada à CPU.Dispatchers.IO
- Usa um pool de 64 threads. Ideal para carga de trabalho vinculada ao IO, onde o thread geralmente está esperando; talvez para solicitação de rede ou leitura / gravação de disco.Dispatchers.Main
(Apenas Android): Usa thread principal para executar as corrotinas. Ideal para atualizar elementos de interface do usuário.
Exemplo
Escrevi um pequeno fragmento de demonstração com 6 funções correspondentes aos 6 cenários acima. Se você executar o fragmento abaixo em um dispositivo Android; abra o fragmento e depois deixe o fragmento; você notará que apenas as corrotinas GlobalScope ainda estão vivas. As corrotinas do ciclo de vida são canceladas por lifecycleScope quando o ciclo de vida é inválido. Por outro lado, os CoroutineScope são cancelados na onPause()
invocação que é feita explicitamente por nós.
class DemoFragment : Fragment() {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
init {
printGlobalScopeWithIO()
printGlobalScope()
printCoroutineScope()
printCoroutineScopeWithMain()
printLifecycleScope()
printLifecycleScopeWithIO()
}
override fun onPause() {
super.onPause()
coroutineScope.cancel()
}
private fun printGlobalScopeWithIO() = GlobalScope.launch(Dispatchers.IO) {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[GlobalScope-IO] I'm alive on thread ${Thread.currentThread().name}!") } } private fun printGlobalScope() = GlobalScope.launch { while (isActive) { delay(1000) Log.d("CoroutineDemo", "[GlobalScope] I'm alive on ${Thread.currentThread().name}!")
}
}
private fun printCoroutineScope() = coroutineScope.launch {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[CoroutineScope] I'm alive on ${Thread.currentThread().name}!") } Log.d("CoroutineDemo", "[CoroutineScope] I'm exiting!") } private fun printCoroutineScopeWithMain() = coroutineScope.launch(Dispatchers.Main) { while (isActive) { delay(1000) Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[CoroutineScope-Main] I'm exiting!")
}
private fun printLifecycleScopeWithIO() = lifecycleScope.launch(Dispatchers.IO) {
while (isActive) {
delay(1000)
Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm alive on ${Thread.currentThread().name}!") } Log.d("CoroutineDemo", "[LifecycleScope-IO] I'm exiting!") } private fun printLifecycleScope() = lifecycleScope.launch { while (isActive) { delay(1000) Log.d("CoroutineDemo", "[LifecycleScope] I'm alive on ${Thread.currentThread().name}!")
}
Log.d("CoroutineDemo", "[LifecycleScope] I'm exiting!")
}
}
Você deve saber que se deseja iniciar a suspend
função, você precisa fazê-lo em CoroutineScope
. Cada um CoroutineScope
tem CoroutineContext
. Onde CoroutineContext
está um mapa que pode conter Dispatcher
(despacha o trabalho para a thread apropriada), Job
(controla o ciclo de vida da co-rotina), CoroutineExceptionHandler
(trata as exceções não capturadas), CoroutineName
(nome da co-rotina, útil para depuração).
GlobalScope.launch(Dispatchers.IO) {}
-GlobalScope.launch
cria co-rotinas globais e usa para operações que não devem ser canceladas, mas uma alternativa melhor seria criar um escopo personalizado na classe Application e injetá-lo na classe que precisa dele. Isso tem a vantagem de permitir que você useCoroutineExceptionHandler
ou substitua oCoroutineDispatcher
para teste.GlobalScope.launch{}
- mesmo queGlobalScope.launch(Dispatchers.IO) {}
, mas runscoroutines
onDispatchers.Default
.Dispatchers.Default
é um padrãoDispatcher
usado se nenhum distribuidor for especificado em seu contexto.CoroutineScope(Dispatchers.IO).launch{}
- é criar escopo com um parâmetro e lançar novocoroutine
nele noIO
thread. Será destruído com o objeto onde foi lançado. Mas você deve ligar manualmente.cancel()
paraCoroutineScope
se você quer terminar o seu trabalho corretamente.lifecycleScope.launch(Dispatchers.IO){}
- são escopos existentes que estão disponíveis em umLifecycle
ou em umLifecycleOwner
(Activity
ouFragment
) e vêm em seu projeto com dependênciaandroidx.lifecycle:lifecycle-runtime-ktx:*
. Usando-o, você pode se livrar da criação manualCoroutineScope
. Ele executará seu trabalhoDispatchers.IO
sem bloqueioMainThread
e certifique-se de que seus trabalhos serão cancelados quando o seulifecycle
for destruído.lifecycleScope.launch{}
- o mesmolifecycleScope.launch(Dispatchers.IO){}
que criaCoroutinesScope
para você com oDispatchers.Main
parâmetro padrão e executa o seucoroutines
paraDispatcher.Main
que você possa trabalharUI
.