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.
También es válido usar una tarea desacoplada cuando NO se quiere esperar por el resultado NI bloquear al actor disparador.
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)