Concurrencia y multiproceso en Swift: nivel avanzado
La concurrencia (concurrency) es uno de los temas más complejos e importantes en el desarrollo para iOS/macOS. A nivel avanzado, Swift ofrece herramientas potentes: desde el clásico Grand Central Dispatch (GCD) hasta el modelo moderno async/await y el estricto sistema de Actores. En este artículo, analizaremos cómo gestionar hilos de manera eficiente, evitar condiciones de carrera y escribir código seguro para hilos.
1. Grand Central Dispatch (GCD): trabajo con colas y grupos
GCD sigue siendo la base del multiproceso en Swift. Sin embargo, a nivel avanzado es importante entender no solo la diferencia entre main y global(), sino también saber gestionar dependencias de tareas.
Creación de colas propias
let serialQueue = DispatchQueue(label: "com.pythonlib.serial")let concurrentQueue = DispatchQueue(label: "com.pythonlib.concurrent", attributes: .concurrent)
serialQueue.async { print("Tarea 1 en cola secuencial")}concurrentQueue.async { print("Tarea 2 en cola concurrente")}DispatchGroup: sincronización de múltiples tareas
let group = DispatchGroup()let queue = DispatchQueue.global()
queue.async(group: group) { sleep(1) print("Carga de datos 1")}queue.async(group: group) { sleep(2) print("Carga de datos 2")}
group.notify(queue: DispatchQueue.main) { print("Todas las cargas completadas, actualizando UI")}Usa DispatchGroup cuando necesites esperar la finalización de varias operaciones paralelas (por ejemplo, carga de imágenes o análisis JSON).
2. Modelo moderno async/await y Task
A partir de Swift 5.5, el lenguaje incorporó soporte nativo para asincronía mediante las palabras clave async y await. Esto simplifica la lectura del código y elimina la "pirámide de closures".
Declaración y llamada de una función asíncrona
func fetchUserData() async -> String { // Simulación de solicitud de red try? await Task.sleep(nanoseconds: 2_000_000_000) return "Datos de usuario cargados"}
Task { let result = await fetchUserData() print(result) // Muestra "Datos de usuario cargados"}Llamadas paralelas con async let
func loadImage() async -> Data { return Data() }func loadMetadata() async -> String { return "meta" }
Task { async let image = loadImage() async let metadata = loadMetadata() let result = try await (image, metadata) print("Imagen: \\(result.0), metadatos: \\(result.1)")}async let permite ejecutar varias tareas asíncronas en paralelo y esperar a que todas terminen. Esto es más eficiente que una llamada secuencial.
3. Actores: seguridad en hilos a nivel de lenguaje
Los Actores (Actors) son un nuevo mecanismo para proteger contra condiciones de carrera. Garantizan que el acceso a su estado se sincronice automáticamente.
Definición y uso de un actor
actor BankAccount { private var balance: Double = 0.0 func deposit(amount: Double) { balance += amount print("Saldo después del depósito: \\(balance)") } func withdraw(amount: Double) -> Bool { guard balance >= amount else { return false } balance -= amount return true } func getBalance() -> Double { balance }}
Task { let account = BankAccount() await account.deposit(amount: 100) let success = await account.withdraw(amount: 50) print("Retiro exitoso: \\(success)")}Todos los métodos del actor están aislados por defecto; solo se pueden llamar mediante await. Esto evita la modificación accidental de datos desde múltiples hilos.
Métodos no aislados (nonisolated)
actor Logger { nonisolated func log(message: String) { print("Log: \\(message)")\