DEV Community

GoyesDev
GoyesDev

Posted on

SC #10: Tarea desacoplada

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
}
Enter fullscreen mode Exit fullscreen mode

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)")
}
Enter fullscreen mode Exit fullscreen mode

El código anterior puede producir el siguiente resultado, aunque no se garantiza el orden:

1
3
2
Enter fullscreen mode Exit fullscreen mode

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)")
  }
}
Enter fullscreen mode Exit fullscreen mode

La salida de este ejemplo es:

Empezando tarea 1
longRunningAsyncOperation(_:) - Tarea 1 falló con error: CancellationError()
Empezando tarea 2
Terminé la tarea
Enter fullscreen mode Exit fullscreen mode

En este ejemplo:

  1. El primer llamado a longRunningAsyncOperation es cancelado correctamente porque se invoca dentro del contexto estructurado del Task cancelado.
  2. En el segundo llamado a longRunningAsyncOperation, incluso aunque se haga try 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()
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)