Concurrencia y subprocesamiento múltiple en Swift: nivel avanzado
\n\nLas aplicaciones modernas para iOS y macOS requieren un alto rendimiento y capacidad de respuesta de la interfaz. El trabajo avanzado con concurrencia en Swift va mucho más allá del simple uso de Grand Central Dispatch (GCD). En este artículo, exploraremos las herramientas clave de Swift 5.5+ para gestionar tareas asíncronas: async/await, Actores, TaskGroup y el protocolo Sendable. Aprenderá cómo evitar condiciones de carrera, estructurar correctamente el código de subprocesamiento múltiple y optimizar el rendimiento.
1. Fundamentos de async/await y Concurrencia Estructurada
\n\nLas palabras clave async y await son la base de la concurrencia moderna en Swift. Permiten escribir código asíncrono que se ve como síncrono, lo que simplifica enormemente la lectura y la depuración.
Declaración de una función asíncrona:
\nfunc fetchUserData() async throws -> User {\n let url = URL(string: "https://api.example.com/user")!\n let (data, _) = try await URLSession.shared.data(from: url)\n let decoder = JSONDecoder()\n return try decoder.decode(User.self, from: data)\n}\n\nLlamada a una función asíncrona con manejo de errores:
\nTask {\n do {\n let user = try await fetchUserData()\n print("User name: \\(user.name)")\n } catch {\n print("Failed to fetch user: \\(error)")\n }\n}\n\nPuntos importantes:
\n- \n
- Concurrencia Estructurada: Cada tarea asíncrona tiene un ciclo de vida claro, limitado por el ámbito de la
Taskpadre. \n - Cancelación de tareas: Cancelación automática de tareas hijas al cancelar la Task padre. \n
- Prioridades: Use
Task(priority: .userInitiated)para tareas importantes para el usuario. \n
2. Actores: protección contra condiciones de carrera
\n\nAntes de Swift 5.5, los desarrolladores solían usar colas serializadas de GCD para proteger el estado compartido. Los Actores proporcionan aislamiento incorporado: el acceso al estado del actor solo es posible a través de await, lo que garantiza la seguridad de subprocesos a nivel del compilador.
Ejemplo de Actor para una cuenta bancaria:
\nactor BankAccount {\n private var balance: Double = 0.0\n\n func deposit(amount: Double) {\n balance += amount\n }\n\n func withdraw(amount: Double) throws {\n guard balance >= amount else {\n throw BankError.insufficientFunds\n }\n balance -= amount\n }\n\n func getBalance() -> Double {\n return balance\n }\n}\n\n// Uso\nlet account = BankAccount()\nTask {\n await account.deposit(amount: 100.0)\n let currentBalance = await account.getBalance()\n print("Balance: \\(currentBalance)")\n}\n\nVentajas de los Actores:
\n- \n
- Verificación del compilador: Intentar acceder a una propiedad del actor sin
awaitgenerará un error de compilación. \n - Reentrada (Reentrancy): Un actor puede pausar la ejecución del método actual (por ejemplo, al usar
awaitdentro del actor) y procesar otra llamada, lo que previene bloqueos. \n - Rendimiento: Los actores utilizan mecanismos de sincronización optimizados, a menudo más rápidos que los bloqueos manuales. \n
3. TaskGroup: procesamiento paralelo de datos
\n\nCuando necesita ejecutar varias operaciones asíncronas independientes y esperar a que todas terminen, TaskGroup es la herramienta ideal. Gestiona automáticamente la cantidad de tareas paralelas y recopila los resultados.
Ejemplo de descarga paralela de varias imágenes:
\nfunc downloadImages(urls: [URL])