GlobalScope vs CoroutineScope vs lifecycleScope
Estoy acostumbrado a trabajar con él AsyncTasky lo entiendo bastante bien por su sencillez. Pero Coroutinesme confunden. ¿Puede explicarme de forma sencilla cuál es la diferencia y el propósito de cada uno de los siguientes?
GlobalScope.launch(Dispatchers.IO) {}GlobalScope.launch{}CoroutineScope(Dispatchers.IO).launch{}lifecycleScope.launch(Dispatchers.IO){}lifecycleScope.launch{}
Respuestas
Primero, comencemos con las definiciones para dejarlo claro. Si necesita un tutorial o un patio de juegos para Coroutines y Coroutines Flow, puede consultar este tutorial / patio de juegos que creé.
Scope es el objeto que usa para lanzar corrutinas que solo contiene un objeto que es 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
}
El contexto de corrutina es un conjunto de reglas y configuraciones que definen cómo se ejecutará la corrutina. Debajo del capó, es una especie de mapa, con un conjunto de posibles claves y valores.
El contexto de la corrutina es inmutable, pero puede agregar elementos a un contexto usando el operador más, al igual que agrega elementos a un conjunto, produciendo una nueva instancia de contexto
El conjunto de elementos que definen el comportamiento de una corrutina son:
- CoroutineDispatcher: envía el trabajo al hilo apropiado.
- Trabajo: controla el ciclo de vida de la corrutina.
- CoroutineName: nombre de la corrutina, útil para depurar.
- CoroutineExceptionHandler: maneja excepciones no detectadas
Distribuidores Los distribuidores determinan qué grupo de subprocesos se debe utilizar. La clase Dispatchers también es CoroutineContext que se puede agregar a CoroutineContext
Dispatchers.Default : trabajo intensivo en CPU, como ordenar listas grandes, realizar cálculos complejos y similares. Un grupo compartido de subprocesos en la JVM lo respalda.
Dispatchers.IO : creación de redes o lectura y escritura de archivos. En resumen, cualquier entrada y salida, como dice el nombre.
Dispatchers.Main : despachador obligatorio para realizar eventos relacionados con la interfaz de usuario en el hilo principal o de la interfaz de usuario de Android.
Por ejemplo, mostrar listas en un RecyclerView, actualizar Vistas, etc.
Puede consultar los documentos oficiales de Android para obtener más información sobre los despachadores.
Editar A pesar de que el documento oficial establece queDispatchers.IO: este despachador está optimizado para realizar E / S de disco o red fuera del hilo principal. Los ejemplos incluyen el uso del componente Room, leer o escribir en archivos y ejecutar cualquier operación de red.
Respuesta de Marko Topolnic
IO ejecuta la corrutina en un grupo de subprocesos especial y flexible. Existe solo como una solución alternativa cuando se ve obligado a usar una API de IO heredada que bloquea su hilo de llamada.
podría tener razón tampoco.
Trabajo Una corrutina en sí misma está representada por un Trabajo. Un trabajo es un identificador de una corrutina. Por cada corrutina que crea (por lanzamiento o asincrónico), devuelve una instancia de trabajo que identifica de manera única la corrutina y administra su ciclo de vida. También puede pasar un trabajo a un CoroutineScope para controlar su ciclo de vida.
Es responsable del ciclo de vida, la cancelación y las relaciones entre padres e hijos de la rutina. Un trabajo actual se puede recuperar del contexto de una corrutina actual: Un trabajo puede pasar por un conjunto de estados: Nuevo, Activo, Completado, Completado, Cancelando y Cancelado. si bien no tenemos acceso a los estados en sí, podemos acceder a las propiedades de un trabajo: isActive, isCancelled y isCompleted.
CoroutineScope Se define una función de fábrica simple que toma CoroutineContexts como argumentos para crear un envoltorio alrededor del 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)"
}
y crea un Jobelemento si el contexto de proporcionar aún no lo tiene.
Veamos el código fuente de 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 puedes ver se extiende CoroutineScope
1- GlobalScope está vivo mientras su aplicación esté viva, si realiza un conteo, por ejemplo, en este alcance y gira su dispositivo, continuará la tarea / proceso.
GlobalScope.launch(Dispatchers.IO) {}
se ejecuta mientras su aplicación esté viva pero en el hilo IO debido al uso Dispatchers.IO
2- Es igual que el primero pero por defecto, si no tienes ningún contexto, el lanzamiento usa EmptyCoroutineContext que usa Dispatchers.Default, así que la única diferencia es el hilo con el primero.
3- Este es el mismo que el primero con solo diferencia de sintaxis.
4- lifecycleScopees una extensión LifeCycleOwnery está vinculada al ciclo de vida de Actvity o Fragmento donde el alcance se cancela cuando se destruye esa Actividad o Fragmento.
/**
* [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
También puede usar esto 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()
}
}
Organizaría su lista en tres ejes:
GlobalScopevs.CoroutineScope()vs.lifecycleScopeDispatchers.IOcontra despachador heredado (implícito)- Especifique el despachador en el alcance vs.como argumento para
launch
1. Elección del alcance
Una gran parte de la visión de Kotlin sobre las corrutinas es la concurrencia estructurada , lo que significa que todas las corrutinas están organizadas en una jerarquía que sigue sus dependencias. Si está iniciando algún trabajo en segundo plano, asumimos que espera que sus resultados aparezcan en algún momento mientras la "unidad de trabajo" actual todavía está activa, es decir, el usuario no se ha alejado de ella y ya no le importa su resultado.
En Android, tiene lifecycleScopea su disposición el que sigue automáticamente la navegación del usuario a través de las actividades de la interfaz de usuario, por lo que debe usarlo como el padre del trabajo en segundo plano cuyos resultados serán visibles para el usuario.
También puede tener algún trabajo de disparar y olvidar, que solo necesita terminar eventualmente, pero el usuario no espera su resultado. Para esto, debe usar las WorkManagerfunciones de Android o similares que puedan continuar de manera segura incluso si el usuario cambia a otra aplicación. Por lo general, estas son tareas que sincronizan su estado local con el estado guardado en el lado del servidor.
En esta imagen, GlobalScopees básicamente una trampilla de escape de la concurrencia estructurada. Le permite satisfacer la forma de suministrar un alcance, pero anula todos los mecanismos que se supone que debe implementar. GlobalScopenunca se puede cancelar y no tiene padre.
Escribir CoroutineScope(...).launches simplemente incorrecto porque crea un objeto de alcance sin un padre que olvida inmediatamente y, por lo tanto, no tiene forma de cancelarlo. Es similar a usar GlobalScopepero aún más hacky.
2. Elección del despachador
El despachador de corrutinas decide en qué subprocesos se puede ejecutar su corrutina. En Android, hay tres despachadores que deberían interesarle:
Mainejecuta todo en el único hilo de la GUI. Debería ser tu principal elección.IOejecuta la corrutina en un grupo de subprocesos especial y flexible. Existe solo como una solución alternativa cuando se ve obligado a usar un legado, bloqueando la API de IO que bloquearía su hilo de llamada.Defaulttambién usa un grupo de subprocesos, pero de tamaño fijo, igual al número de núcleos de CPU. Úselo para trabajos de computación intensiva que tomarían el tiempo suficiente para causar una falla en la GUI (por ejemplo, compresión / descompresión de imágenes).
3. Dónde especificar el despachador
Primero, debe conocer el despachador especificado en el alcance de la corrutina que está utilizando. GlobalScopeno especifica ninguno, por lo que el valor predeterminado general está en vigor, el Defaultdespachador. lifecycleScopeespecifica el Maindespachador.
Ya explicamos que no debe crear ámbitos ad-hoc utilizando el CoroutineScopeconstructor, por lo que el lugar adecuado para especificar un despachador explícito es como parámetro launch.
En detalle técnico, cuando escribe someScope.launch(someDispatcher), el someDispatcherargumento es en realidad un objeto de contexto de corrutina completo que tiene un solo elemento, el despachador. La corrutina que está iniciando crea un nuevo contexto para sí misma al combinar el que está en el alcance de la corrutina y el que usted proporciona como parámetro. Además de eso, crea un fresco Jobpara sí mismo y lo agrega al contexto. El trabajo es hijo del heredado en el contexto.
TL; DR
GlobalScope.launch (Dispatchers.IO) : inicia una corrutina de nivel superior en
Dispatchers.IO. Coroutine no está ligado y sigue funcionando hasta que finaliza o se cancela. A menudo se desaconseja, ya que el programador debe mantener una referencia ajoin()ocancel().GlobalScope.launch : Igual que el anterior, pero se
GlobalScopeusaDispatchers.Defaultsi no se especifica. A menudo desanimado.CoroutineScope (Dispatchers.IO) .launch : Crea un alcance de corrutina que se usa a
Dispatchers.IOmenos que se especifique un despachador en el constructor de corrutinas, es decirlaunchCoroutineScope (Dispatchers.IO) .launch (Dispatchers.Main) : Bonificación uno. Utiliza el mismo alcance de corrutina que el anterior (¡si la instancia del alcance es la misma!) Pero anula
Dispatcher.IOconDispatchers.Mainpara esta corrutina.lifecycleScope.launch (Dispatchers.IO) : inicia una corrutina dentro del lifecycleScope proporcionado por AndroidX. Coroutine se cancela tan pronto como se invalida el ciclo de vida (es decir, el usuario navega fuera de un fragmento). Se utiliza
Dispatchers.IOcomo grupo de subprocesos.lifecycleScope.launch : Igual que el anterior, pero se usa
Dispatchers.Mainsi no se especifica.
Explicación
El alcance de la corrutina promueve la concurrencia estructurada , por lo que puede lanzar varias corrutinas en el mismo alcance y cancelar el alcance (que a su vez cancela todas las corrutinas dentro de ese alcance) si es necesario. Por el contrario, una co-rutina GlobalScope es similar a un hilo, donde es necesario mantener una referencia en el fin de join()o cancel()ella. Aquí hay un excelente artículo de Roman Elizarov en Medium .
CoroutineDispatcher le dice al constructor de corrutinas (en nuestro caso launch {}) qué grupo de subprocesos se utilizará. Hay algunos despachadores predefinidos disponibles.
Dispatchers.Default- Utiliza un grupo de subprocesos equivalente al número de núcleos de CPU. Debe utilizarse para cargas de trabajo vinculadas a la CPU.Dispatchers.IO- Utiliza un grupo de 64 subprocesos. Ideal para cargas de trabajo vinculadas a E / S, donde el hilo suele estar esperando; tal vez para solicitud de red o lectura / escritura de disco.Dispatchers.Main(Solo Android): usa el hilo principal para ejecutar las corrutinas. Ideal para actualizar elementos de la interfaz de usuario.
Ejemplo
Escribí un pequeño fragmento de demostración con 6 funciones correspondientes a los 6 escenarios anteriores. Si ejecuta el siguiente fragmento en un dispositivo Android; abra el fragmento y luego deje el fragmento; notará que solo las corrutinas de GlobalScope siguen activas. LifecycleScope cancela las corrutinas del ciclo de vida cuando el ciclo de vida no es válido. Por otro lado, los de CoroutineScope se cancelan en la onPause()invocación que hacemos explícitamente por nosotros.
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!")
}
}
Debe saber que si desea iniciar la suspendfunción, debe hacerlo en formato CoroutineScope. Cada uno CoroutineScopetiene CoroutineContext. ¿Dónde CoroutineContexthay un mapa que puede contener Dispatcher(despacha el trabajo al hilo apropiado), Job(controla el ciclo de vida de la corrutina), CoroutineExceptionHandler(maneja las excepciones no detectadas), CoroutineName(nombre de la corrutina, útil para depurar).
GlobalScope.launch(Dispatchers.IO) {}-GlobalScope.launchcrea corrutinas globales y se usa para operaciones que no deben cancelarse, pero una mejor alternativa sería crear un alcance personalizado en la clase Application e inyectarlo en la clase que lo necesita. Esto tiene la ventaja de darle la posibilidad de usarCoroutineExceptionHandlero reemplazar elCoroutineDispatcherpara pruebas.GlobalScope.launch{}- igual queGlobalScope.launch(Dispatchers.IO) {}pero se ejecutacoroutinesenDispatchers.Default.Dispatchers.Defaultes un valor predeterminadoDispatcherque se utiliza si no se especifica ningún despachador en su contexto.CoroutineScope(Dispatchers.IO).launch{}- Es crear alcance con un parámetro y lanzar un nuevocoroutineen él en elIOhilo. Será destruido con el objeto donde fue lanzado. Sin embargo, usted debe llamar manualmente.cancel()porCoroutineScopesi desea terminar su trabajo correctamente.lifecycleScope.launch(Dispatchers.IO){}- son los ámbitos existentes que están disponibles desde unLifecycleo desde unLifecycleOwner(ActivityoFragment) y vienen en su proyecto con dependenciaandroidx.lifecycle:lifecycle-runtime-ktx:*. Usándolo puede deshacerse de la creación manualCoroutineScope. Ejecutará su trabajoDispatchers.IOsin bloquearloMainThread, y se asegurará de que sus trabajos se cancelen cuandolifecyclese destruya.lifecycleScope.launch{}- Lo mismo que ellifecycleScope.launch(Dispatchers.IO){}que creaCoroutinesScopepara usted con elDispatchers.Mainparámetro predeterminado y se ejecutacoroutinesenDispatcher.Mainese medio con el que puede trabajarUI.