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 hereda:
- La prioridad del contexto
- Estado de cancelación del contexto.
Para crear la tarea se usa el método estático detached(name:priority:operation:)
Task.detached {
// Ejecuta una operación de forma asíncrona fuera del contexto estructurado de concurrencia
}
Considerar el siguiente ejemplo:
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
Considerar 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 invocar a longRunningAsyncOperation 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
longRunningAsyncOperationes cancelado correctamente porque se invoca dentro del contexto estructurado delTaskcancelado. - En el segundo llamado a
longRunningAsyncOperation, incluso aunque se hagatry Task.checkCancellation(), no hay forma de detectar la cancelación porque precisamente NO estamos heredando este estado.
Para cancelar una Task desacoplada, se debe tener una referencia a ella y cancelarla manualmente. Además, estas no son canceladas automáticamente cuando la referencia se libera, así que sí o sí de la debe cancelar manualmente.
¿Cuándo usar una tarea desacoplada?
Es válido crear una tarea desacoplada en escenarios donde una operación pueda correr de forma independiente y no requiera conexión al contexto contenedor, y sea aceptable que termine a pesar de que el contenedor haya sido cancelado.
En el siguiente ejemplo es válido crear la tarea desacoplada, teniendo en cuenta que no hay ninguna referencia a self:
Task.detached(priority: .background) {
await DirectoryCleaner.cleanup()
}
Top comments (0)