Swift Concurrency: Guía Completa de Task, Async/Await y Actors

La llegada de conceptos como Task, async/await y Actors en Swift ha revolucionado la forma en que escribimos código multihilo. Estas herramientas ofrecen un enfoque mucho más simple y eficiente en comparación con los clásicos DispatchQueue, OperationQueue o Thread.

En este artículo, exploraremos cómo estas nuevas herramientas de concurrencia pueden mejorar drásticamente la calidad y legibilidad de tu código en iOS.

1. El Bloque Task

La unidad básica de trabajo asíncrono en el nuevo modelo de Swift es la Task. Viene a sustituir patrones antiguos como DispatchQueue.main.async {}.

Comparativa: Código antiguo vs. nuevo

Antes, dependíamos de Grand Central Dispatch (GCD) para manejar hilos:

func oldConcurrencyCode() {
    DispatchQueue.main.async {
        // Ejecución asíncrona en el hilo principal
    }
    
    DispatchQueue.global(qos: .background).async {
        // Ejecución en segundo plano
    }
}

Con la nueva sintaxis, el código es mucho más legible y estructurado:

func newConcurrencyCode() {
    Task {
        // Se ejecuta asíncronamente (hereda el contexto del actor actual)
    }

    Task { @MainActor in
        // Se ejecuta explícitamente en el hilo principal
    }

    Task(priority: .background) {
        // Se ejecuta con prioridad de segundo plano
    }
}

2. Async / Await

Async/await es el corazón de la concurrencia moderna en Swift.

  • async: Indica que una función o propiedad es asíncrona.
  • await: Se utiliza para esperar el resultado de una operación asíncrona sin bloquear el hilo actual.

Ejemplo práctico: Petición de red

Olvídate de los complejos cierres (closures) y los problemas de pirámides de la perdición (pyramid of doom).

func doSomeRequest() async throws -> MyModel {
    // Realiza la petición y lanza error si falla
}

@MainActor 
func makesRequestAndUpdateViews() async {
    do {
        let model = try await doSomeRequest()
        view.updateModel(model)
    } catch {
        view.switchErrorState(error)
    }
}

3. Group Operations: Procesamiento en Paralelo

Tradicionalmente, realizar operaciones en grupo requería el uso de DispatchGroup (con sus enter() y leave()), lo cual era propenso a errores. Ahora tenemos TaskGroup.

func readFileInChunks(_ url: URL) async throws -> Data {
    try await withThrowingTaskGroup(of: Data.self) { group in
        let totalSize = totalSize(url)
        for slice in stride(from: .zero, to: totalSize, by: 1_024) {
            group.addTask {
                return readBytesData(slice, 1_024, url)
            }
        }
        
        var data = Data()
        for try await slice in group {
            data.append(slice)
        }
        return data
    }
}

4. Actors: Seguridad de Datos sin Bloqueos Manuales

Los Actors son una herramienta poderosa para proteger el estado de tus objetos. Swift garantiza automáticamente que el acceso a las propiedades y métodos de un actor sea seguro desde múltiples hilos, evitando las «condiciones de carrera» (race conditions).

actor DataManager {
    var array: [Int] = []

    func append(_ element: Int) {
        array.append(element)
    }

    func read() -> Int? {
        array.last
    }
}

5. AsyncStream: Flujos de Datos Asíncronos

AsyncStream funciona de forma similar a un array asíncrono, permitiendo producir y consumir elementos bajo demanda mediante el protocolo AsyncSequence.

var closure: ((Int?) -> Void)?

let sequence = AsyncStream<Int> { continuation in
    closure = { value in
        if let value = value {
            continuation.yield(value)
        } else {
            continuation.finish()
        }
    }
}

// Consumo de la secuencia
Task {
    for await integer in sequence {
        print("Recibido: \(integer)")
    }
}

Conclusión

La adopción de Swift Concurrency no es solo una cuestión de «estar a la moda», sino de escribir código más seguro, fácil de mantener y con menos errores de sincronización. Si aún usas DispatchQueue, es el momento perfecto para empezar a migrar tus proyectos.

Deja una respuesta