SC #10: Tarea desacoplada
Source: Dev.to
Una tarea desacoplada (Detached Task) ejecuta una operación de forma asíncrona, fuera del contexto de concurrencia estructurado que la envuelve. No heredar este contexto implica que no se heredan:
- La prioridad del contexto
- El estado de cancelación del contexto.
Para crear la tarea se usa el método estático Task.detached(name:priority:operation:):
Task.detached {
// Ejecuta una operación de forma asíncrona fuera del contexto estructurado de concurrencia
}
Ejemplo básico
func detachedTaskExample() async {
func asyncFunc(_ string: String) async {
print(string)
}
await asyncFunc("\(1)")
Task.detached {
await asyncFunc("\(2)")
}
await asyncFunc("\(3)")
}
El código anterior puede producir el siguiente resultado, aunque no se garantiza el orden:
1
3
2
Riesgo de cancelación de tarea desacoplada
Considera el siguiente ejemplo, donde longRunningAsyncOperation es una tarea asíncrona que se demora (no termina antes de que se cancele) y detachedTaskExample es una tarea que invoca a longRunningAsyncOperation como subtarea y luego crea un Task.detached para invocarla de forma desacoplada:
func detachedTaskExample() async {
let outerTask = Task {
// Esta subtarea sí se va a cancelar
await longRunningAsyncOperation(1)
// Esta tarea desacoplada NO hereda el estado de cancelación
Task.detached(priority: .background) {
// Por eso este checkCancellation no hace nada
try Task.checkCancellation()
// Por eso esta subtarea no se va a cancelar
await longRunningAsyncOperation(2)
}
}
outerTask.cancel()
}
private func longRunningAsyncOperation(_ id: Int) async {
do {
print("Empezando tarea \(id)")
try await Task.sleep(for: .seconds(5))
print("Terminé la tarea")
} catch {
print("\(#function) - Tarea \(id) falló con error: \(error)")
}
}
La salida de este ejemplo es:
Empezando tarea 1
longRunningAsyncOperation(_:) - Tarea 1 falló con error: CancellationError()
Empezando tarea 2
Terminé la tarea
En este ejemplo:
- El primer llamado a
longRunningAsyncOperationse cancela correctamente porque se invoca dentro del contexto estructurado delTaskcancelado. - En el segundo llamado, aunque se ejecute
try Task.checkCancellation(), no hay forma de detectar la cancelación porque no heredamos ese estado.
Para cancelar una Task desacoplada, se debe mantener una referencia a ella y cancelarla manualmente. Estas tareas no se cancelan automáticamente cuando la referencia se libera, por lo que es necesario gestionarlas explícitamente.
¿Cuándo usar una tarea desacoplada?
Es apropiado crear una tarea desacoplada en escenarios donde:
- La operación puede ejecutarse de forma independiente.
- No requiere conexión al contexto contenedor.
- Es aceptable que continúe ejecutándose aunque el contenedor haya sido cancelado.
Ejemplo válido (no hay referencia a self):
Task.detached(priority: .background) {
await DirectoryCleaner.cleanup()
}