GCD vs Swift Concurrency (async/await, actors) — ¿cuándo usas cuál?
GCD no desapareció y async/await no lo reemplazó por completo — un ingeniero senior sabe qué modelo de concurrencia encaja con cada cola, condición de carrera o API heredada.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
GCD vs Swift Concurrency (async/await, actors) — ¿cuándo usas cuál?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Dos modelos de concurrencia, un mismo código base
La mayoría de los códigos base de iOS reales hoy contienen tanto Grand Central Dispatch — la API de colas basada en C que existe desde iOS 4 — como Swift Concurrency: async/await, tareas estructuradas y actors, introducidos con Swift 5.5 e iOS 15. Un entrevistador que pregunta "¿cuándo usas cuál?" quiere saber si de verdad has enviado a producción código en una base que está a mitad de una migración, no solo si viste la sesión de WWDC.
#GCD, en breve
GCD te entrega colas: DispatchQueue.main para el hilo de la interfaz, DispatchQueue.global(qos:) para trabajo en segundo plano con una calidad de servicio dada, y colas seriales o concurrentes personalizadas que creas tú mismo. El trabajo se envía con .async (dispara y olvida, retorna de inmediato) o .sync (bloquea al llamador hasta que termina — peligroso en la misma cola en la que ya estás corriendo, un deadlock clásico). DispatchGroup te permite lanzar varias piezas de trabajo asíncrono y recibir una notificación cuando todas terminan; DispatchSemaphore limita el acceso a un recurso escaso o conecta código basado en callbacks con una espera bloqueante.
#Dónde GCD se vuelve incómodo
Ninguno de los bloques de construcción de GCD tiene estructura exigida por el compilador — un bloque DispatchQueue.global().async { } puede sobrevivir al objeto que lo inició, nada propaga la cancelación automáticamente, y el estado mutable compartido al que acceden varias colas todavía necesita un lock o una cola serial por la que recuerdes enrutar cada acceso, por convención y no por comprobación del compilador. El código GCD basado en callbacks que compone varios pasos asíncronos degenera en closures anidados — la clásica "pirámide de callbacks" —, y cada uno de esos closures es candidato a un ciclo de retención si captura self fuertemente.
#La respuesta de Swift Concurrency
async/await permite que el código asíncrono se lea de arriba hacia abajo como código síncrono, sin el anidamiento de closures. Un Task es una unidad estructurada de trabajo asíncrono con un ciclo de vida definido — las tareas hijas se cancelan cuando se cancela su padre, y la cancelación es cooperativa, comprobada en los puntos await o explícitamente con Task.checkCancellation() — una mejora real sobre el "nada sabe que este trabajo debería detenerse" de GCD. Los tipos actor te dan acceso serializado a su estado mutable exigido por el compilador: el compilador rechaza el código que accede a las propiedades almacenadas de un actor desde fuera sin un await, cerrando toda una categoría de data races que GCD solo evita por convención. @MainActor declara "esto corre en el hilo principal" como un atributo comprobado en lugar de un salto en tiempo de ejecución que hay que recordar escribir.
#Cuándo GCD sigue ganándose su lugar
Swift Concurrency no borró las razones de existir de GCD. Primitivas de bajo nivel como DispatchSemaphore siguen siendo la herramienta correcta para limitar acceso en código que debe permanecer síncrono, o para conectar APIs síncronas más antiguas. Una base de código con años de infraestructura basada en GCD que no se está reescribiendo por completo es una razón legítima para seguir escribiendo GCD en esa zona en lugar de mezclar paradigmas al azar. Y cualquier SDK basado en callbacks que no controlas — una librería de analítica antigua, un cliente de autenticación heredado — sigue entregándote un closure de finalización, no una función async.
#Conectando ambos mundos
No tienes que elegir un solo paradigma para toda la app de una vez. withCheckedContinuation (o withCheckedThrowingContinuation para la variante que lanza errores) envuelve exactamente una llamada basada en callback dentro de una función async, de modo que los nuevos puntos de llamada se puedan escribir en async/await mientras la implementación heredada por debajo queda intacta. TaskGroup reemplaza a DispatchGroup para lanzar trabajo paralelo estructurado con la misma forma de "esperar a que todos terminen", pero con propagación de cancelación incorporada.
#En código
// GCD: obtiene dos recursos de forma concurrente y luego los une.func loadDashboard(completion: @escaping (Profile, [Post]) -> Void) {let group = DispatchGroup()var profile: Profile!var posts: [Post] = []group.enter()api.fetchProfile { result inprofile = resultgroup.leave()}group.enter()api.fetchPosts { result inposts = resultgroup.leave()}group.notify(queue: .main) {completion(profile, posts)}}
// Swift Concurrency: estructurado, cancelable, se lee de arriba hacia abajo.func loadDashboard() async throws -> (Profile, [Post]) {async let profile = api.fetchProfile()async let posts = api.fetchPosts()return try await (profile, posts)}// Actor: acceso serializado exigido por el compilador, sin locks manuales.actor ImageCache {private var storage: [URL: UIImage] = [:]func image(for url: URL) -> UIImage? {storage[url]}func insert(_ image: UIImage, for url: URL) {storage[url] = image}}// Conectando una API heredada basada en callbacks a async/await, una sola vez.func fetchToken() async throws -> String {try await withCheckedThrowingContinuation { continuation inlegacyAuthClient.requestToken { token, error inif let token {continuation.resume(returning: token)} else {continuation.resume(throwing: error ?? AuthError.unknown)}}}}