Swift y Objective-C

[Swift avanzado #3] Migración de Sendable y los errores de concurrencia de Swift 6

Al activar el modo Swift 6 aparecen errores de concurrencia por todas partes, y Sendable suele ser el protagonista. Este artículo explica qué significa este protocolo y si los valores pueden cruzar límites de aislamiento, con una ruta de migración basada en convertir a struct, hacer inmutable y promover a actor.

7 min de lectura
Imagen de portada de [Swift avanzado #3] Migración de Sendable y los errores de concurrencia de Swift 6

Si tu equipo ha activado el modo Swift 6, probablemente recuerdes el momento: un proyecto que funcionaba correctamente empieza a mostrar decenas o cientos de errores de concurrencia.

El protagonista de la mayoría de los mensajes de error es uno: Sendable.

La tercera entrega de la serie Concurrency organiza esta última pieza del rompecabezas y la strict concurrency de Swift 6.

La pregunta de Sendable — ¿puede este valor cruzar el límite?

En la entrega sobre actor describimos el aislamiento como un «contexto de ejecución serial»: dentro de un actor, sobre MainActor o en un pool cooperativo que no pertenece a ninguno de los dos.

Sin embargo, los valores cruzan estos límites constantemente. Entran como argumentos de métodos de actor, salen como valores de retorno y se capturan en cierres de Task.

Ahí aparece el riesgo. El aislamiento protege el estado del propio actor, pero ¿qué ocurre si el valor que cruza el límite es un tipo por referencia mutable?

La misma instancia puede quedar en manos de dos aislamientos a la vez, y la carrera de datos que el actor bloqueaba revive a través del objeto introducido. Es como tener un control fronterizo estricto, pero no revisar las mercancías que entran.

Sendable define ese criterio de inspección. Es un protocolo marcador sin métodos requeridos, y su significado es único:

Los valores de este tipo pueden usarse simultáneamente de forma segura después de cruzar un límite de aislamiento.

¿Qué tipos son seguros? La intuición es sencilla.

Los tipos por valor se copian al cruzar, así que son seguros si todas sus propiedades almacenadas son Sendable. Aquí entran Int, String y los struct y enum compuestos únicamente por propiedades Sendable. Normalmente el compilador los reconoce automáticamente.

Los actor también son seguros: la autoprotección viene incorporada.

Las clases inmutables (final y solo con propiedades let) también son seguras. Si nada puede cambiar, no hay carreras.

Solo hay algo peligroso: las clases con estado mutable. La fórmula de la entrega sobre priorizar tipos por valor, «compartido + mutable = peligroso», es exactamente el criterio de Sendable.

Los tipos función tienen una anotación @Sendable específica. El cierre de Task { } es un cierre @Sendable típico, y un cierre con esta marca no puede capturar valores non-Sendable.

Como vimos en la entrega sobre cierres, las capturas pueden convertirse en rutas de contrabando que cruzan el aislamiento; por eso el lenguaje inspecciona la propia ruta.

strict concurrency — De advertencias a errores, de disciplina a comprobación

Estas reglas ya existían antes de Swift 6, pero por defecto permanecían silenciosas.

La esencia del modo de lenguaje Swift 6 es activar todas estas comprobaciones y convertirlas en errores: strict concurrency. Con complete checking, cada punto donde un valor non-Sendable cruza un límite de aislamiento se convierte en un error de compilación.

Los errores no aparecen porque el código haya empeorado de repente. Simplemente ahora se hacen visibles carreras potenciales que ya existían.

Es parecido a cuando se introdujeron los opcionales y quedaron expuestos todos los lugares donde un valor podía ser nil. Igual que entonces aparecieron las comprobaciones de nil, ahora las suposiciones sobre concurrencia migran al sistema de tipos.

Por suerte, el compilador también se vuelve más inteligente. Swift 6 incluye el análisis de aislamiento basado en regiones (propuesta Swift Evolution SE-0414, region-based isolation).

Incluso los valores non-Sendable pueden moverse si se demuestra que «quien los envió no volverá a tocarlos».

Un valor cuya propiedad se ha transferido no puede crear una carrera. Por eso mucho código que en teoría debería fallar en realidad compila.

La anotación sending para parámetros sigue la misma dirección. En resumen, la regla se vuelve más precisa: de «prohibido siempre» a «permitido si se demuestra que es seguro».

Diagrama de comprobación: struct, actor y clases final con let pasan; las clases mutables se rechazan
Solo son peligrosas las clases con estado mutable

Migración práctica — Soluciones por tipo de error

La mayoría de los errores se reduce a unos pocos patrones. Estas son las soluciones estándar para cada tipo.

Tipo 1. Mi modelo es non-Sendable. Es el error más común y más saludable. La primera solución es convertirlo en struct.

Si un modelo de datos que no necesita identidad de referencia se declaró como class, esta es la ocasión para trasladarlo a un tipo por valor.

Si debe ser class, hazlo inmutable con final + let y adopta Sendable. Si debe ser mutable, necesita un propietario del estado, así que considera promoverlo a actor.

Tipo 2. Variables globales y static var. Todos los lugares que antes señalábamos diciendo que «static var es, en la práctica, estado global» pasan a ser errores.

Si realmente necesitas estado global, declara su aislamiento. Añade @MainActor para lo relacionado con la UI; en otros casos, envuélvelo en un actor o conviértelo en let inmutable.

Tipo 3. Una clase de delegado o callback queda atrapada en el límite. Suele aparecer al encontrarse con API de la época de UIKit.

Si el tipo es, en la práctica, exclusivo del hilo principal, declarar @MainActor suele ser la respuesta. Convierte en explícito el hecho implícito de que «esta clase siempre se usó en el principal».

Tipo 4. Es realmente seguro, pero el compilador no lo sabe. Por ejemplo, clases protegidas internamente con bloqueos y wrappers de bibliotecas C.

La salida es @unchecked Sendable. Declara «yo garantizo que es seguro, así que desactiva la comprobación», pero unchecked es una herramienta de la familia unsafe, como advierte su nombre.

Siguiendo el principio de salida explícita de la primera entrega de Filosofía, deja en comentarios la base de la garantía (qué protege cada bloqueo) y úsala en el ámbito mínimo posible.

Si empiezas a cubrir errores con unchecked por comodidad durante la migración, acabarás con código cuyo chequeo está desactivado, pero que lleva la insignia de Swift 6.

No es necesario activarlo todo de una vez. El modo de lenguaje puede seleccionarse por módulo.

El enfoque estándar es ascendente: subir primero a Swift 6 los módulos hoja con pocas dependencias, como utilidades y modelos, y dejar el target de la app para el final.

Las banderas de funciones próximas de Xcode también sirven para elevar antes solo el nivel de comprobación y observar las advertencias.

Entender la dirección — ¿por qué nos hace pasar por esto?

Como el dolor de la migración es real, conviene entender exactamente qué justifica este coste.

La promesa de Swift 6 es: si compila, no hay carreras de datos. Categorías enteras de crashes intermitentes irreproducibles y errores de temporización que solo aparecen después del lanzamiento desaparecen en tiempo de compilación.

Es el mismo camino que siguió la seguridad de memoria (opcionales, ARC — Automatic Reference Counting, conteo automático de referencias). La seguridad de concurrencia pasa de ser «algo que se logra escribiendo bien» a «algo que garantiza el lenguaje».

Esta dirección prolonga también la trayectoria de la serie de filosofía: elevar reglas propensas a errores al sistema de tipos, exigir anotaciones explícitas donde tienen un coste (@unchecked, sending) y absorber la fricción de transición mediante adopción gradual (modos de lenguaje por módulo).

Los procedimientos vistos en la entrega sobre Swift Evolution siguen reduciendo la fricción al añadir mecanismos de amortiguación, como opciones de aislamiento predeterminadas. Las advertencias actuales son el punto intermedio de esa transición.

Ilustración de señales de las etapas de migración de Swift 5 a un Swift 6 correcto
Conversión a struct→inmutabilidad→actor→MainActor; usar unchecked al final y con justificación

Resumen

  • Sendable marca los tipos cuyos valores pueden usarse simultáneamente con seguridad al cruzar límites de aislamiento. Los tipos por valor, actor y las clases inmutables son seguros; las clases mutables concentran todo el peligro.
  • Los cierres @Sendable comprueban sus capturas e impiden que se conviertan en rutas de contrabando para carreras.
  • Swift 6 strict concurrency eleva estas comprobaciones a errores. La explosión de errores no significa que el código haya empeorado; significa que han salido a la luz carreras potenciales.
  • Prioridad de soluciones: convertir a struct → hacer inmutable → promover a actor → declarar el aislamiento (@MainActor) → por último, usar @unchecked Sendable con la justificación documentada.
  • Migra desde los módulos hoja hacia arriba y eleva el modo de lenguaje módulo a módulo.

La próxima entrega cierra la serie Concurrency con la concurrencia estructurada. Trata el árbol de tareas formado por Task, async let y TaskGroup, y cómo cancellation se propaga por él.


Referencias

Seguir leyendo