Swift 6 introdujo un nuevo enfoque a la concurrencia en aplicaciones.En este artículo, exploraremos los problemas que pretende resolver, explicaremos cómo funciona bajo el capó, compararemos el nuevo modelo con el anterior, y echaremos un vistazo más de cerca al modelo Actor.En las próximas partes, también descomponeremos ejecutores, programadores, concorrencia estructurada, diferentes tipos de ejecutores, implementaremos nuestro propio ejecutor, y más. Revisión general de la competencia de Swift: problemas y soluciones Escribir código que ejecute tareas simultáneamente puede mejorar el rendimiento y la capacidad de respuesta, pero a menudo introduce complejidad y errores sutiles como condiciones de carreras, deadlocks y problemas de seguridad. Swift Concurrency, introducido en Swift 6, tiene como objetivo simplificar la programación simultánea al proporcionar un modelo claro, seguro y eficiente para el manejo de tareas asíncronas. 💡Si usa Swift 5.x y planea migrar a Swift 6, puede habilitar las verificaciones de Concurrencia de Swift en sus configuraciones de proyecto. Esto le permite adoptar gradualmente el nuevo modelo de concurrencia mientras mantiene la compatibilidad con el código existente. Activar estas verificaciones ayuda a capturar posibles problemas de concurrencia temprano, haciendo la transición más suave y segura. A medida que actualiza su base de códigos, puede comenzar a integrar la sintaxis async/await y otras características de Concurrencia de Swift incrementalmente sin una reescritura completa. 💡Si usa Swift 5.x y planea migrar a Swift 6, puede habilitar los controles de concurrencia de Swift en sus configuraciones de proyecto. Esto le permite adoptar gradualmente el nuevo modelo de concurrencia mientras mantiene la compatibilidad con el código existente. async/await sintaxis y otras funciones de Swift Concurrency incrementalmente sin una reescritura completa. Some of the key problems Swift Concurrency addresses include: Condiciones de la raza: Impedir el acceso simultáneo al estado mutable compartido que pueda causar comportamiento impredecible. Callback Hell: Simplifica el código asincrónico que solía depender en gran medida de los llamados o manejadores de finalización, haciendo que el código sea más fácil de leer y mantener. Complexidad de gestión de threads: Abstracción de la creación y sincronización de bajo nivel, permitiendo a los desarrolladores centrarse en la lógica en lugar de manejar los threads. Coordinación de tareas simultáneas: La concurrencia estructurada permite jerarquías claras de tareas con cancelación y propagación de errores adecuados. Con la introducción de nuevos idiomas como Swift 6 ofrece una forma más intuitiva y robusta de escribir código simultáneo, mejorando tanto la productividad de los desarrolladores como la estabilidad de las aplicaciones. async/await Multitasking Los sistemas operativos modernos y los tiempos de ejecución utilizan multitarea para ejecutar unidades de trabajo simultáneamente. Swift Concurrency adopta multitarea cooperativa, que difiere fundamentalmente del modelo de multitarea preemptiva utilizado por los hilos de nivel de sistema operativo. Prevención Multitasking El multitarea preventiva es el modelo utilizado por los sistemas operativos para gestionar los threads y procesos. En este modelo, un programador de nivel de sistema puede interrumpir forzadamente cualquier thread en prácticamente cualquier momento para realizar un cambio de contexto y asignar el tiempo de la CPU a otro thread. El multitarea preventiva permite el verdadero paralelismo entre varios núcleos de la CPU y evita que los hilos mal comportados o de larga duración monopolicen los recursos del sistema. Sin embargo, esta flexibilidad viene a un costo. Debido a que los hilos pueden interrumpirse en cualquier momento de su ejecución —incluso en medio de una operación crítica—, los desarrolladores deben utilizar primitivos de sincronización como mutexes, semáforos o operaciones atómicas para proteger el estado mutable compartido. Este modelo ofrece un mayor control y concurrencia cruda, pero también pone una carga significativamente mayor en los desarrolladores.Asegurar la seguridad del hilo en un entorno preventivo es propenso a errores y puede conducir a un comportamiento no determinista -comportamiento que varía de ejecutar a ejecutar- que es notoriamente difícil de razonar o probar de manera fiable. Desde un punto de vista técnico, el multitarea preemptiva depende del sistema operativo para manejar la ejecución del hilo. El sistema operativo puede interrumpir un hilo en casi cualquier punto —incluso en el medio de una función— y cambiar a otra. Para ello, el sistema debe realizar un interruptor de contexto, que implica guardar el estado de ejecución completo del hilo actual (como los registros de la CPU, el indicador de instrucciones y el indicador de pilas), y restaurar el estado previamente guardado de otro hilo. These operations introduce significant runtime overhead. Each context switch takes time and consumes system resources — especially when context switches are frequent or when many threads compete for limited CPU cores. Additionally, preemptive multitasking forces developers to write thread-safe code by default, increasing overall complexity and the risk of concurrency bugs. Si bien este modelo proporciona la máxima flexibilidad y el verdadero paralelismo, a menudo es excesivo para los flujos de trabajo asíncronos, donde las tareas suelen pasar la mayor parte de su tiempo esperando I/O, entrada de usuario o respuestas de red en lugar de utilizar activamente la CPU. Multitasking cooperativo En contraste, el tiempo de ejecución concurrente de Swift utiliza multitasking cooperativo. En este modelo, una tarea se ejecuta hasta que libera voluntariamente el control, típicamente en un punto de espera o a través de una llamada explícita a A diferencia de los enlaces tradicionales, las tareas cooperativas nunca se preemptan forzadamente. Esto resulta en una ejecución predecible: los cambios de contexto ocurren sólo en puntos de suspensión claramente definidos. Task.yield() Las tareas de cooperación de Swift están programadas en un grupo de thread cooperativo ligero, gestionado por el tiempo de ejecución, separado de las filas de Grand Central Dispatch. Se espera que las tareas que se ejecutan en este grupo sean "buenos ciudadanos", proporcionando control cuando sea necesario, especialmente durante el trabajo de ejecución prolongada o intenso en CPU. como punto de suspensión manual, asegurando que otras tareas tengan una oportunidad de ejecutar. Task.yield() Sin embargo, el multitarea cooperativa viene con una advertencia: si una tarea nunca se suspende, puede monopolizar el hilo en el que se ejecuta, retrasando o hambriento otras tareas en el sistema. En el multitarea cooperativa, la unidad fundamental de ejecución no es un hilo, sino un pedazo de trabajo, a menudo llamado una continuación.Una continuación es un segmento suspendido de una función asíncrona. La función se suspende en un , el tiempo de ejecución de Swift captura el estado de ejecución actual en una continuación asignada por pila. Esta continuación representa un punto de reanudación y está encuadrada para la ejecución futura. async await En lugar de asociar un hilo con una tarea de larga duración, el tiempo de ejecución Swift trata un hilo como un tubo de continuos. Cada hilo ejecuta una continuación tras otra. Cuando una continuación termina o se suspende de nuevo, el hilo recoge la siguiente continuación lista de la cola. Como se mencionó anteriormente, este modelo evita los interruptores de contexto tradicionales a nivel de sistema operativo. No hay necesidad de guardar y restaurar registros de CPU o pilas de filamentos; el tiempo de ejecución simplemente invoca la próxima continuación similar a la de cierre. Esto hace que el cambio de tareas sea muy rápido y ligero, aunque implica una mayor asignación de pilas para almacenar el estado asíncano suspendido. El compromiso clave: se utiliza un poco más de memoria pero se obtiene un superávit dramáticamente menor para la gestión de tareas.La planificación cooperativa da un control estricto sobre cuando ocurren suspensión, lo que mejora la predictibilidad y hace que la concurrencia sea más fácil de razonar. Introducción a Tareas Tareas En competición rápida, a proporciona una unidad de trabajo asíncrono. a diferencia de simplemente llamar a un La función, a es un objeto gestionado que se ejecuta simultáneamente con otras tareas en un grupo de thread cooperativo. Task async Task 💡 Las tareas son gestionadas por un grupo de filamentos cooperativos. El grupo de filamentos cooperativos está diseñado para gestionar la concurrencia de manera eficiente al permitir que las tareas produzcan la CPU mientras esperan que las operaciones asíncronas se completen. 💡 Las tareas son gestionadas por un grupo de filamentos cooperativos. El grupo de filamentos cooperativos está diseñado para gestionar la concurrencia de manera eficiente al permitir que las tareas produzcan la CPU mientras esperan que las operaciones asíncronas se completen. Las tareas se pueden crear para ejecutarse simultáneamente, y también se pueden esperar o cancelar, proporcionando un control fino sobre el comportamiento asíncrono y son una parte integral de la concurrencia estructurada en Swift. Creando a Task Una tarea puede ser creada utilizando el , which immediately launches the provided asynchronous operation: Task Inicializador Inicializador Task(priority: .userInitiated) { await fetchData() } Cuando se crea un utiliza el inicializador estándar (es decir, no ), hereda el contexto, la prioridad y los valores de tareas locales de los actores circundantes.Este comportamiento es crucial para la concorrencia estructurada y la seguridad en el código concurrente. Task Separados Separados 💡 Swift 6.2 introduce un cambio significativo en la forma en que se maneja la concurrencia: por defecto, todo el código se ejecuta en MainActor. Para ejecutar el código en un fondo, Swift 6.2 añade un nuevo atributo @concurrent. También puede usar no aislado si el código no requiere acceso al actor principal. Swift 6.2 introduce un cambio significativo en la forma en que se maneja la concurrencia: por defecto, todo el código se ejecuta en MainActor Para ejecutar el código en un fondo, Swift 6.2 añade un nuevo atributo @concurrent También puedes utilizar nonisolated si el código no requiere acceso al actor principal. WWDC Embracing Swift Concurrency WWDC incorpora a la competencia Swift WWDC incorpora a la competencia Swift Bajo el capó, en versiones anteriores de Swift Concurrency, el tiempo de ejecución de Swift usó un mecanismo interno llamado Aunque esta propiedad no formaba parte de la API pública, desempeñó un papel clave en asegurar que las tareas creadas dentro del código aislado por el actor se ejecutaran en el mismo actor, preservando la seguridad de la carrera de datos. @_inheritActorContext Con los avances en Swift, el tiempo de ejecución ha comenzado a Un nuevo sistema conocido como , que ahora es más explícitamente gestionado por el compilador y runtime. @_inheritActorContext sending Enviar es una nueva palabra clave introducida en Swift 6 como parte del movimiento del lenguaje hacia una concurrencia más segura y explícita. Se utiliza para marcar parámetros de función y devolver valores que se mueven a través de límites de concurrencia. Funciona especialmente bien tipos no copiables, garantizando la seguridad de la memoria y previniendo errores de desplazamiento después del uso. Cuando un parámetro está marcado con enviar, el compilador impone que la instancia original ya no sea accesible después de la transferencia. func process(_ data: sending MyNonCopyableType) async { // `data` is moved here and can’t be used elsewhere after the call } Enviar es una nueva palabra clave introducida en Swift 6 como parte del movimiento del lenguaje hacia una concurrencia más segura y explícita. Se utiliza para marcar parámetros de función y devolver valores que se mueven a través de límites de concurrencia. Funciona especialmente bien tipos no copiables, garantizando la seguridad de la memoria y previniendo errores de desplazamiento después del uso. Cuando un parámetro está marcado con enviar, el compilador impone que la instancia original ya no sea accesible después de la transferencia. func process(_ data: sending MyNonCopyableType) async { // `data` is moved here and can’t be used elsewhere after the call } Cuando se lanza a , debe asegurarse de que cualquier valor capturado por la tarea es El compilador ahora aplica esto en el tiempo de compilación usando El protocolo y el Tipo de función. Task.detached Sendable Sendable @Sendable incapacidad de conformarse con puede resultar en un error de tiempo de compilación, particularmente en el modo de concurrencia estricta. Sendable Las tareas también soportan prioridades, similar a cómo las colas de Grand Central Dispatch las manejan. vs Task Task.detached Al trabajar con Swift Concurrency, es importante comprender la diferencia entre y , ya que definen cómo y dónde se ejecuta el trabajo asíncrono. Task Task.detached Task Tareas hereda el contexto actual de los actores (como or any custom actor) and priority. It’s commonly used when you want to spawn a new asynchronous operation that still respects the current structured concurrency tree or actor isolation. This is especially useful for UI updates or working inside specific concurrency domains. Task MainActor Task { await updateUI() } En el ejemplo de arriba, si se llama desde el actor principal, el will also run on the main actor unless explicitly moved elsewhere. Task Task.detached Task.desactivado Crea una tarea completamente independiente. No hereda el contexto o la prioridad del actor actual. Esto significa que comienza en un contexto simultáneo global y requiere gestionar la seguridad, especialmente cuando se accede a datos compartidos. Task.detached Task.detached { await performBackgroundWork() } Uso cuando necesita ejecutar operaciones de fondo fuera del contexto estructurado actual, como realizar cálculos de larga duración o escapar del aislamiento de un actor. Task.detached Colaboración en Thread Pool A Cooperative Thread Pool in Swift Concurrency is a mechanism that manages the execution of asynchronous tasks by scheduling them onto a limited number of threads, typically matching the number of CPU cores. Un Cooperative Thread Pool en Swift Concurrency es un mecanismo que gestiona la ejecución de tareas asíncronas programándolas en un número limitado de hilos, típicamente coincidiendo con el número de núcleos de CPU. Swift Concurrency opera utilizando un grupo de enlaces cooperativos diseñados para una planificación eficiente y una superposición mínima de enlaces. A diferencia del modelo tradicional de ejecución de enlaces por tareas, el enfoque de Swift enfatiza la concordancia estructurada y la planificación consciente de los recursos. Una supersimplificación común es decir que Swift Concurrency utiliza un hilo por núcleo, que se alinea con su objetivo de reducir el cambio de contexto y maximizar la utilización de la CPU. Título: No es tan sencillo Título: No es tan sencillo En un Mac de 16 núcleos, es posible observar hasta 64 hilos gestionados por Swift Concurrency solo - sin la participación de GCD. Esto se debe a que el pool de hilos cooperativo de Swift no sólo mapea por núcleo, sino por núcleo por bucket de QoS. de forma oficial: Max threads = (CPU cores) × (dedicated quality-of-service buckets) Así, en un sistema de 16 núcleos: 16 cores × 4 QoS buckets = 64 threads Cada bucket de QoS es esencialmente una línea de thread dedicada para un grupo de tareas que comparten una prioridad de ejecución similar. Estas son gestionadas internamente por el mecanismo de programación de thread de Darwin y no son las mismas que las filas de GCD. QoS Buckets y prioridad de tareas QoS Buckets y prioridad de tareas Aunque Exponen seis constantes, algunas de las cuales son aliases: TaskPriority Inicialmente elevado Utilidad baja Default → ya mapeado a medio Para la perspectiva del núcleo, esto simplifica a 4 niveles de prioridad de núcleo, cada uno mapeado a un bucket de QoS, lo que influye en la asignación de thread en el pool de thread cooperativo. ¿Cuándo ocurre el Overcommit? ¿Cuándo ocurre el Overcommit? Bajo la carga normal, Swift Concurrency respeta los límites del pool cooperativo. Sin embargo, en el caso de contención (por ejemplo, tareas de alta prioridad esperando por tareas de baja prioridad), el sistema puede sobrecomprometerse con los hilos para preservar la capacidad de respuesta. Este comportamiento es gestionado por el núcleo de Darwin a través de políticas de planificación de Mach y de alta prioridad. cadenas - no algo controlado explícitamente por su código. pthreads Tareas de prioridad Swift proporciona un sistema de prioridad para tareas, similar al Grand Central Dispatch (GCD), pero más semánticamente integrado en el modelo de concurrencia estructurada. Inicializador : Task Task(priority: .userInitiated) { await loadUserData() } Las prioridades disponibles están definidas por la Enum : TaskPriority Priority Description / .high .userInitiated For tasks initiated by user interaction that require immediate feedback. .medium For tasks that the user is not actively waiting for. / .low .utility For long-running tasks that don’t require immediate results, such as copying files or importing data. .background For background tasks that the user is not directly aware of. Primarily used for work the user cannot see. / de .high .userInitiated Para tareas iniciadas por la interacción del usuario que requieren un feedback inmediato. .medium Para tareas que el usuario no está esperando activamente. / de .low .utility Para tareas de larga duración que no requieren resultados inmediatos, como copiar archivos o importar datos. .background For background tasks that the user is not directly aware of. Primarily used for work the user cannot see. Desarrollar tareas con diferentes prioridades Desarrollar tareas con diferentes prioridades Cuando se crea un dentro de otra tarea (default prioridad), puede establecer explícitamente una prioridad diferente para cada tarea envuelta. , y el otro es .high. Esto demuestra que las prioridades se pueden establecer individualmente independientemente del padre. Task .medium .low Task { // .medium by default Task(priority: .low) { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .high) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } // 1, thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}, priority: TaskPriority.low // 2, thread: <_NSMainThread: 0x6000017040c0>{number = 1, name = main}, priority: TaskPriority.high If you don’t explicitly set a priority for a nested task, it inherits the priority of its immediate parent. In this example, the anonymous tasks inside y Los bloques heredan esas prioridades respectivas a menos que se superen. .high .low Las prioridades de tareas se pueden heredar Las prioridades de tareas se pueden heredar Task { Task(priority: .high) { Task { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") Task { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } } // 2, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.low // 1, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.high // 3, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.low // 4, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}, priority: TaskPriority.medium Si no establece explícitamente una prioridad para una tarea envuelta, ésta hereda la prioridad de su madre inmediata. En este ejemplo, las tareas anónimas dentro de los bloques .high y .low heredan esas prioridades respectivas a menos que se superponga. Escalada de la prioridad Escalada de la prioridad Task(priority: .high) { Task { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } await Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") await Task { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") }.value Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } }.value } // 1, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 2, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 3, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.high // 4, thread: <_NSMainThread: 0x6000017000c0>{number = 1, name = main}, priority: TaskPriority.medium Este mecanismo se llama escalada de prioridad: cuando una tarea está esperando por una tarea de mayor prioridad, el sistema puede aumentar temporalmente su prioridad para evitar barreras y garantizar la capacidad de respuesta. As a result: Task 2, which is , is escalated to while being awaited. .low .high Tareas 3, que no tiene una prioridad explícita, hereda la prioridad escalada de su prioridad principal (Tareas 2) y también se ejecuta con .highpriority. Tareas 4 establece explícitamente su prioridad a .medium, por lo que no es afectado por la escalada. No hereda la prioridad Task.detached Task.desactivado Detached tasks ( ) ejecutan de forma independiente y no heredan la prioridad de su tarea principal. Se comportan como tareas globales con su propia programación. Esto es útil para aislar el trabajo de fondo, pero también puede conducir a desajustes de prioridades inesperadas si no se establece manualmente. Task.detached Task(priority: .high) { Task.detached { print("\(1), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .low) { print("\(2), "thread: \(Thread.current)", priority: \(Task.currentPriority)") Task.detached { print("\(3), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } Task(priority: .medium) { print("\(4), "thread: \(Thread.current)", priority: \(Task.currentPriority)") } } } // 1, thread: <NSThread: 0x60000174dec0>{number = 4, name = (null)}, priority: TaskPriority.medium // 2, thread: <_NSMainThread: 0x600001708180>{number = 1, name = main}, priority: TaskPriority.low // 3, thread: <NSThread: 0x60000174dec0>{number = 4, name = (null)}, priority: TaskPriority.medium // 4, thread: <_NSMainThread: 0x600001708180>{number = 1, name = main}, priority: TaskPriority.medium Suspension Points and How Swift Manages Async Execution In Swift, any call to an Funciones Usando is a potential suspension point - a place in the function where executing might pause and resume latter. It’s a transformation that involves saving the state of the function so it can be resumed latter, after awaited operation completes. async await Here’s an example: func fetchData() async -> String { let result = await networkClient.load() return result } En este caso, es un punto de suspensión.Cuando la función alcanza esta línea, puede parar la ejecución, dar control al sistema, y más tarde reanudar una vez finishes. Behind the scenes, the compiler transforms this function into a state machine that tracks its progress and internal variables. await networkClient.load() load() Bajo el sombrero: continuos y máquinas de estado Every La función en Swift se compila en una máquina de estado. marca un punto de transición. antes de llegar a un En el Swift: async await await Almacena el estado actual de la función, incluidas las variables locales y el indicador de instrucción actual. Suspende la ejecución y programa una continuación. Una vez que la operación async se completa, se retoma la función desde donde se detuvo. Esto es similar al estilo de paso de continuidad (CPS) utilizado en muchos sistemas de programación funcional. En el modelo de concurrencia de Swift, esto está orquestado por tipos internos como y el calendario de competición. ParticialAsyncTask Suspensión! = Bloqueo Cuando tú algo en Swift, el hilo actual no está bloqueado, en su lugar: await La tarea actual devuelve el control al ejecutor Otras tareas se pueden ejecutar mientras espera Cuando la operación esperada se completa, la tarea suspendida se reanuda en el ejecutor correspondiente. Esto hace fundamentalmente más eficiente y escalable que las operaciones de bloqueo basadas en hilos como . async/await DispatchQueue.sync • Permitir la ejecución de otras tareas Task.yield() Task.yield de la empresa() Es un método estático proporcionado por el sistema de concurrencia de Swift que suspende voluntariamente la tarea actual, dándole al sistema la oportunidad de ejecutar otras tareas enqueadas. Task.yield() func processLargeBatch() async { for i in 0..<1_000_000 { if i % 10_000 == 0 { await Task.yield() } } } sin , este ciclo monopolizaría al ejecutor. Periódicamente, usted está cooperando con el tiempo de ejecución de concurrencia de Swift, lo que permite mantener la respuesta y la equidad. await await Task.yield() Bajo el Hood Llamar a esperar suspende la tarea actual y la reencuentra al final de la cola para su ejecutor actual (por ejemplo, el actor principal o un ejecutor simultáneo global). Task.yield() Es parte del modelo de multitarea cooperativa de Swift: las tareas se ejecutan hasta el siguiente punto de suspensión y se espera que tengan un rendimiento equitativo. A diferencia de los sistemas preventivos (por ejemplo, los hilos), las tareas de Swift no se interrumpen forzadamente - deben entregar voluntariamente el control. Summary Swift 6 marca un paso importante hacia adelante en la forma en que se maneja la concurrencia, ofreciendo a los desarrolladores más control, predictibilidad y seguridad.Aunque la curva de aprendizaje puede ser empinada al principio, comprender estos conceptos abre la puerta para construir aplicaciones altamente responsivas y robustas.A medida que continuamos explorando los aspectos más profundos del nuevo modelo de concurrencia de Swift, está claro que estos cambios ponen las bases para el futuro del desarrollo de aplicaciones seguras y escalables.