Swift Concurrency Series 03 | Cómo entender Task, Task.detached y MainActor
A menudo aparecen juntos, pero responden a tres preguntas respectivamente: "¿De dónde viene la tarea, quién está vinculado a ella y quién cambia la interfaz de usuario?"
Cuando entré en contacto por primera vez con Swift Concurrency, estaba confundido acerca de Task, Task.detached y MainActor:
- Todo lo relacionado con asíncrono.
- a menudo aparecen en el mismo fragmento de código
- Todo parece “dejar que el código se ejecute en alguna parte”
Entonces, la forma más común de aprender es memorizar definiciones. Sin embargo, si sólo confía en las definiciones para recordar estos tres conceptos, es fácil confundirse a medida que los aprende. Debido a que parecen conceptos similares, en realidad responden a tres tipos de preguntas completamente diferentes.
Una forma más práctica de entenderlo es:
Task: Cómo iniciar la misiónTask.detached: Qué tan estrechamente está vinculada la tarea al contexto actualMainActor: ¿En qué semántica aislada se debe ejecutar esta lógica?
Simplemente rompa estas tres dimensiones y gran parte de la confusión desaparecerá inmediatamente.
1. Task resuelve: ¿Cómo ingreso al proceso asincrónico desde aquí?
Los escenarios de uso más naturales para Task son:
El código actual no es
async, pero necesito ingresar a un proceso asincrónico ahora.
Esto es muy común en iOS:
- La devolución de llamada al hacer clic en el botón no es
async - El método de proxy UIKit no es
async - Un determinado evento del ciclo de vida sincrónico desencadena repentinamente una solicitud asincrónica
Por ejemplo:
Button("刷新") {
Task {
await viewModel.reload()
}
}
El significado de Task aquí es muy claro: unir un portal de interacción sincrónico al mundo asincrónico.
Entonces, la clave para Task es “se crea el límite de la tarea”.
Una vez escrito significa:
- Aquí comienza un trabajo asincrónico con un ciclo de vida independiente.
- puede ser cancelado
- Terminará en algún momento.
- En última instancia, alguien tiene que ocuparse de sus consecuencias.
Esto también muestra que Task no puede entenderse simplemente como “se puede usar una capa para await”.
2. Las características reales del Task ordinario: Suele continuar un período de trabajo asincrónico en el contexto actual
Una situación común es entender el Task ordinario demasiado “independientemente”, pensando que una vez creado, no tiene nada que ver con el contexto actual.
Una comprensión más precisa en ingeniería debería ser:
**Común Task es a menudo una tarea que se extiende desde el contexto actual. **
Esto significa que a menudo hereda parte del entorno actual, como por ejemplo:
- Semántica actual del actor.
- Contexto de la tarea actual
- Ciertas cancelaciones de relaciones
- Tendencias prioritarias actuales
Por lo tanto, es más como “ingresar asíncrono además de la lógica actual” en lugar de “crear un nuevo universo completamente fuera de sintonía”.
Es importante comprender esto, porque más adelante comprenderá de qué se está “desconectando” exactamente el Task.detached.
3. Task.detached es una declaración independiente más sólida
Muchos artículos describen Task.detached como una versión “más avanzada”, lo que fácilmente puede generar sesgos en la práctica.
Es más peligroso.
La razón es sencilla: en esencia, es menos probable que herede el contexto actual.
Es decir, al escribir:
Task.detached {
...
}
De hecho, está expresando:
- Este trabajo no quiere quedar naturalmente ligado al contexto actual.
- Quiero que sea más independiente.
- Estoy dispuesto a asumir yo mismo más responsabilidades de ciclo de vida y aislamiento.
Esto es razonable en algunos escenarios, como por ejemplo:
- Realizar trabajos de limpieza de fondo que poco tengan que ver con la página actual.
- Realizar ciertas tareas que obviamente requieren romper con la semántica actual del actor.
- Ciertos marcos o capas de infraestructura requieren explícitamente tareas independientes.
Pero en el negocio de las páginas, lo que la mayoría de la gente realmente quiere es algo más manejable.
Y detached a menudo simplemente dificulta la gestión.
4. Task.detached se usa mal fácilmente
Porque el primer sentimiento que da a la gente es “libertad”.
Pero en los sistemas concurrentes, el precio de la libertad suele ser el desbordamiento de responsabilidades.
Una vez que utilices el Task.detached, pronto tendrás que volver a responder estas preguntas:
-¿A quién pertenece ahora?
- ¿Debería continuar cuando se destruya la página?
- Si se cancela la tarea externa, ¿seguirá ejecutándose?
- ¿Puede cambiar directamente el estado actual después de que regrese?
Si ninguna de estas preguntas tiene una respuesta clara, Task.detached generalmente termina con un escape de la responsabilidad de la misión.
Entonces mi principio predeterminado es simple:
- Utilice primero el
Tasknormal para las capas de página y ViewModel. - Sólo considere
Task.detachedsi tiene muy claro “por qué es necesario romper con el contexto actual”
5. MainActor no es el concepto de “iniciar una tarea” en absoluto.
Éste es el punto que es necesario aclarar más a fondo.
Task y Task.detached discuten:
Cómo se crea una tarea asincrónica y qué tan estrechamente está vinculada al contexto actual.
MainActor analiza:
Bajo qué semántica de aislamiento se debe ejecutar un determinado fragmento de código.
No es una “versión del hilo principal de la tarea”, ni una “tarea utilizada específicamente para actualizar la interfaz de usuario”. Básicamente le dice al compilador y a la persona que llama:
- Esta lógica pertenece al dominio de aislamiento del actor principal.
- Está fuertemente relacionado con la interfaz de usuario.
- No se puede modificar aleatoriamente en ningún contexto concurrente
Por lo tanto, el enfoque de MainActor siempre ha sido las “limitaciones”.
6. Los códigos relacionados con la interfaz de usuario deben tomarse en serio MainActor
Una situación común es pensar: De todos modos, al final, simplemente asignamos un valor a la página, por lo que no debería ser tan grave.
El problema es que a un estado de interfaz de usuario verdaderamente complejo nunca se le asigna un valor solo una vez.
Las páginas reales suelen tener estas cosas coexistiendo:
- estado de carga
- Lista de datos
- Estado vacío
- mensaje de error
- Algunos botones locales están deshabilitados.
Una vez que estos valores se escriben mediante múltiples resultados asincrónicos en diferentes momentos, sin límites claros de MainActor, el problema se acumulará lentamente en:
- La página parpadea ocasionalmente.
- Algunas actualizaciones de estado están en un orden extraño
- ViewModel va y viene entre la lógica de fondo y la lógica de la interfaz de usuario
Por lo tanto, el valor de MainActor no es solo “prevenir errores de subproceso”, sino también establecer un límite de propiedad claro para los estados de la interfaz de usuario.
7. Una secuencia de juicio más cercana al combate real.
Si no puede determinar qué concepto utilizar al escribir código, primero puede preguntarse en el siguiente orden:
1. ¿Estoy en un contexto no asíncrono y necesito ingresar a un proceso asíncrono?
Si es así, considere primero Task.
2. ¿Realmente necesito que esta tarea exista independientemente del contexto actual?
Considere únicamente Task.detached si la respuesta es muy clara.
Si simplemente “se siente más libre”, normalmente no debería usarse.
3. ¿Este código de lectura y escritura de la interfaz de usuario está fuertemente relacionado con el estado?
Si es así, debería considerar seriamente MainActor en lugar de esperar a que algo salga mal.
Esta secuencia de juicio es mucho más útil que memorizar las definiciones de API porque corresponde directamente a los problemas reales que se enfrentan al escribir código comercial.
8. ¿Cómo es el mal olor más común?
Los tres tipos de malos olores más comunes que veo son:
1. Dondequiera que await no esté disponible, empaquete primero un Task
Esto hará que las entradas de tareas en el proyecto estén cada vez más fragmentadas y, al final, nadie podrá saber quién es el propietario de estas tareas.
2. No entiendo la herencia de contexto y siempre uso Task.detached
Esto parece muy “independiente”, pero en realidad a menudo simplemente empuja aún más el problema del ciclo de vida.
3. ViewModel es responsable tanto del procesamiento en segundo plano como de la reescritura de la interfaz de usuario, pero no existe un límite claro MainActor
Es posible que este tipo de código no necesariamente falle en el corto plazo, pero es particularmente propenso a acumular errores de estado ocultos en el largo plazo.
9. Conclusión: No están en la misma dimensión
Si tuviera que recordar estos tres conceptos en una frase, diría esto:
Task: ahora quiero crear una tarea asincrónicaTask.detached: Quiero crear una tarea asincrónica que sea más independiente y menos heredada del contexto actual.MainActor: Esta lógica debe ejecutarse dentro del límite de aislamiento del actor principal.
Responden diferentes preguntas en sistemas concurrentes:
- ¿Dónde comienza la misión?
- Qué tan estrechamente está ligada la tarea al contexto actual.
- Qué estados deben ser aislados por el actor principal
Mientras estas tres dimensiones estén separadas, cuando escriba código de concurrencia Swift más adelante, “iniciar una tarea” y “volver al hilo principal” ya no se confundirán con lo mismo.
What to read next
Want more posts about Swift Concurrency?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Swift Concurrency?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home