Back home

Swift Concurrency Series 01 | La razón por la que Swift presenta async/await

Es el intento de Swift de mejorar la concurrencia de "según la convención" a "capacidad del lenguaje".

Cuando vea async/await por primera vez, lo entenderá como una “versión mejorada de escritura de devolución de llamada”.

Esta comprensión es sólo a medias correcta.

Si solo desea escribir código más corto y un estilo más sincrónico, otros lenguajes ya han hecho cosas similares. Lo que Swift realmente quiere resolver es que el antiguo modelo asincrónico se ha vuelto cada vez más difícil de soportar la complejidad de los proyectos iOS modernos.

En el pasado, es posible que solo necesitara procesar un clic en un botón para enviar una solicitud, pero ahora una página normal puede implicar:

  • Cargar múltiples recursos en paralelo en la primera pantalla
  • Cancelar tareas antiguas al salir de la página.
  • La caché local y la solicitud remota compiten por el mismo estado.
  • Actualizar automáticamente el token después de que expire el estado de inicio de sesión
  • Las actualizaciones de la interfaz de usuario del hilo principal y el procesamiento en segundo plano se producen de forma intercalada.

Una vez que el negocio alcanza esta complejidad, el problema asincrónico ya no es solo un problema de escritura, sino un problema de diseño del sistema. La importancia de async/await es precisamente que hace avanzar este tipo de problemas de “hábitos a nivel de biblioteca” a “reglas a nivel de lenguaje”.

1. El verdadero problema con el modelo antiguo es que el flujo de control está roto.

A todo el mundo le gusta utilizar el “infierno de devolución de llamada” para explicar los problemas del antiguo método de escritura asincrónica, pero si sólo se detiene en “la sangría es demasiado profunda”, todavía no entiende el punto.

El verdadero problema con las devoluciones de llamada es que el negocio ** es originalmente una línea, pero el código se divide en muchos fragmentos discretos. **

Por ejemplo, un proceso de inicialización muy común:

  1. Extraiga la información del usuario actual
  2. Determine el módulo de la página de inicio según los permisos del usuario.
  3. Extraiga los datos de la página de inicio nuevamente.
  4. Si falla, registra los puntos ocultos.
  5. Finalmente regrese al hilo principal para actualizar la interfaz de usuario.

En mi opinión, este es un vínculo muy claramente secuenciado. Pero en la era de la finalización, este vínculo a menudo se divide en:

  • un cierre de solicitud
  • Un cierre de sentencia de permiso
  • Varias capas de manejo de errores
  • Un interruptor de hilo principal
  • Varias sucursales de devolución anticipada.

A medida que el código se hace más largo, resulta difícil explicarlo de un vistazo:

  • ¿Cuál es el proceso principal?
  • ¿Qué ramas se romperán?
  • ¿Quién debería asumir la culpa cuando falla un paso?
  • ¿Deberías continuar después de salir de la página?

La verdadera dificultad para mantener la lógica asincrónica es que la intención y la expresión están fuera de contacto.

2. El mayor problema con la finalización es que deja demasiadas semánticas clave a la convención.

completarlo no es algo malo. Muchas de las API subyacentes siguen siendo muy útiles hoy en día y siguen siendo adecuadas en ciertos escenarios que requieren múltiples devoluciones de llamada, transmisión de salida y conexión de sistemas más antiguos.

El problema es que cuando un sistema depende en gran medida de la finalización, gran parte de la semántica importante son simplemente “hábitos de equipo” en lugar de reglas del lenguaje.

Por ejemplo, lo siguiente suele entenderse de forma predeterminada en el modelo de finalización:

  • ¿Esta devolución de llamada se llamará una o varias veces?
  • ¿El éxito y el fracaso son estrictamente excluyentes entre sí?
  • ¿En qué hilo volverá la devolución de llamada?
  • Si la persona que llama tiene la capacidad de cancelar
  • Si el objeto se suelta a mitad de camino, ¿se deben seguir entregando los resultados?

Descubrirá que estos son los problemas centrales de los sistemas concurrentes.

Una vez que esta semántica se mantiene mediante la documentación, la denominación y la experiencia, cuanto más grande sea el proyecto, más fácil será que la semántica diverja. Lo más doloroso al final suele ser la incapacidad de razonar de manera estable sobre cómo se ejecutará un fragmento de código asincrónico.

3. Swift introduce async/await, que básicamente refuerza las reglas del código asincrónico.

El valor de async/await no se trata sólo de:

fetchUser { result in
  ...
}

Reemplazar con:

let user = try await fetchUser()

Más importante aún, devuelve al nivel del idioma muchas cosas que originalmente estaban dispersas en la convención.

Por ejemplo ahora:

  • Una función async tiene un punto de retorno claro
  • El error se propaga a través de throw en lugar de mediante un estilo diferente de devolución de llamada de resultados.
  • await indica explícitamente que este es el punto de pausa
  • La cadena de llamadas asincrónicas se puede leer sin saltar entre múltiples cierres.

La importancia de este asunto no es “queda bien”, sino que “las reglas son más estrictas”.

El verdadero temor a los sistemas complejos son siempre las reglas laxas. Una vez que se aflojan las reglas, el código se vuelve cada vez más dependiente de “esta persona simplemente entiende el contexto”.

4. Mejora no sólo la legibilidad, sino también la razonabilidad.

Una situación común es: puedo entender la finalización, ¿por qué tengo que aprender async/await?

La pregunta no es si puedes entenderlo hoy, sino:

  • ¿Aún puedes entenderlo rápidamente después de tres meses?
  • Cuando los colegas asumen el control, ¿pueden deducir el proceso basándose en el propio código?
  • Cuando ocurre un error, ¿es posible caminar desde la entrada hasta la salida?

Ésta es la diferencia entre “legibilidad” y “razonabilidad”.

La legibilidad es “¿Este código es agradable a mis ojos hoy?” La razonabilidad es “¿Puedo juzgar con base en este código: cómo se transmite la falla, cómo surte efecto la cancelación y en qué nivel se cambia el estado?”.

Por ejemplo, los siguientes problemas suelen ser muy difíciles de resolver en el modelo antiguo:

  • Después de que el usuario abandona la página, ¿sigue existiendo esta cadena de solicitudes?
  • Cuando falla el tercer paso, ¿dónde terminará el estado de la página final?
  • Cuando aparecen dos resultados asincrónicos al mismo tiempo, ¿quién está calificado para escribir la interfaz de usuario?

async/await ciertamente no responderá automáticamente estas preguntas para el equipo, pero al menos permite que estas preguntas se coloquen en una estructura de proceso más clara en lugar de ocultarse en cierres fragmentados.

5. Este asunto se está volviendo cada vez más crítico en los proyectos de iOS.

Uno de los mayores cambios en el proyecto iOS actual es que “la asincronía ha pasado de una capacidad marginal a una capacidad troncal”.

En el pasado, asincrónico consistía más en “hacer clic y enviar una solicitud”. Ahora participa directamente de forma asincrónica en el ciclo de vida de la página, la máquina de estado, la capa de caché, el sistema de permisos, el sistema de puntos ocultos e incluso participa en el diseño de la velocidad de respuesta de todo el producto.

Una situación común para una página comercial real es:

  • Habrá una solicitud de primera pantalla tan pronto como se abra la página.
  • Algunos módulos usan caché local para evadir
  • Algunas solicitudes se basan en perfiles de usuario o configuraciones experimentales.
  • Ciertas operaciones requieren prevención de reentrada
  • Algunos resultados deben devolverse al hilo principal para cambiar el estado de forma segura

Bajo esta premisa, si el sistema asincrónico sigue siendo simplemente “cada uno escribe su propia finalización”, el código pronto pasará de “puede funcionar” a “nadie se atreve a cambiar”.

Entonces, lo que realmente capta async/await es que asíncrono se ha convertido en el método de trabajo predeterminado en las aplicaciones modernas.

6. Es la entrada a todo el modelo de concurrencia de Swift.

Si solo observa esta característica gramatical, async/await parece ser simplemente una mejor expresión asincrónica.

Pero si analizamos Swift Concurrency en su conjunto, en realidad está allanando el camino para capacidades posteriores:

  • Task: Aclarar los límites de las tareas y el ciclo de vida
  • MainActor: Restringir la semántica de ejecución de estados relacionados con la interfaz de usuario
  • Actor: Aislar estado mutable compartido
  • Simultaneidad estructurada: incorporar subtareas en el ciclo de vida de la tarea principal.

En otras palabras, Swift no solo intenta proporcionar una API asincrónica más conveniente, sino que también intenta convertir la “concurrencia” de una técnica fragmentada en un conjunto de modelos componibles, razonables y verificables por lenguaje.

Esto también muestra que async/await no puede considerarse simplemente “más fácil de escribir”. Lo que realmente está conectado detrás de esto es un conjunto completamente nuevo de filosofías de diseño de concurrencia.

7. No resuelve automáticamente todos los problemas, pero cambia la naturaleza del problema.

También debe quedar claro aquí: con async/await, los errores de concurrencia no desaparecerán automáticamente.

No resuelve:

  • Diseño incorrecto de las fronteras estatales.
  • Las tareas antiguas sobrescriben los nuevos resultados.
  • Un ViewModel asume demasiadas responsabilidades al mismo tiempo.
  • La estrategia de cancelación de la misión no está pensada en absoluto.

Pero cambia una cosa muy importante: Muchos problemas ya no son “porque la expresión es demasiado confusa y ni siquiera puedo ver cómo se ve el problema”, sino “el problema se ha expresado claramente”.

Esto puede parecer un paso atrás, pero en realidad es un gran paso adelante. Porque sólo cuando el problema se vuelve expresable la depuración, revisión y refactorización pueden ser efectivas.

8. Conclusión: Swift introduce async/await para no escribir menos cierres

Para decirlo de forma más breve, diría:

Swift presenta async/await para hacer que el código asincrónico sea comprensible, razonable y mantenible nuevamente.

Por tanto, es el primer paso para actualizar el modelo de concurrencia.

Una vez que comprenda esto, cuando mire Task, Actor y MainActor más adelante, no los considerará como puntos de sintaxis dispersos, pero comprenderá que lo que Swift está haciendo es algo más importante: **Reclasificar la concurrencia de “habilidades empíricas” a “reglas lingüísticas”. **