DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Relación entre Hilos y Tasks

Preguntas

¿Qué es exactamente un hilo y por qué tiene un alto costo de creación y cambio de contexto?

Un hilo es un recurso, manejado por el sistema operativo, que ejecuta una secuencia de instrucciones. Crear hilos y cambiar el contexto entre ellos es una operación muy costosa.

Cada hilo requiere su propia pila de memoria (stack) y el cambio de contexto obliga al CPU a guardar y restaurar el estado completo del hilo (registros, puntero de instrucción, etc).

¿Por qué Swift Concurrency no garantiza en qué hilo se ejecutará una función asíncrona?

Swift Concurrency dispone de un pool global concurrente de hilos (tantos como el número de procesadores del dispositivo).

Para optimizar el uso de hilos, se despacha una Task en cualquier hilo disponible. Cuando hay un punto de suspensión (await), el runtime de Swift Concurrency cede el hilo a cualquier otra tarea. Luego, cuando la Task recupera el contexto, el runtime de SC le asigna cualquier hilo disponible - que puede ser el que tenía inicialmente, pero no está garantizado, porque el sistema pudo haberlo asignado a otra Task.

Aquí quien limita los hilos no es el sistema operativo sino el framework Swift Concurrency. El sistema operativo podría crear muchos más hilos (como lo hacía GCD). Es una decisión de diseño del framework, no una restricción del sistema operativo.

¿Qué ocurre exactamente cuando una tarea llega a un punto de suspensión con await?

El sistema suspende la Task y devuelve el hilo al pool global de hilos - Lo que significa que está disponible para que cualquier otra Task lo tome.

¿Cómo funciona el cooperative thread pool y por qué limita los hilos al número de núcleos de CPU?

Para no tener "thread explosion" y así degradar el desempeño de la aplicación debido al cambio de contexto entre hilos y posibles problemas de inversión de prioridades, el sistema operativo limita la creación de hilos al número de núcleos del procesador.

El "cooperative thread pool" son todos los hilos disponibles para ser usados por las distintas Tasks.

¿Qué es el thread explosion y cómo lo evita Swift Concurrency?

"thread explosion" ocurre porque se crean demasiados hilos y aparecen bloqueos que provocan:

  1. Desperdicio de memoria debido a los hilos detenidos.
  2. Intercambio excesivo de contexto (que reduce la eficiencia del procesador)
  3. Problemas de inversión de prioridad.

¿Por qué tener menos hilos no reduce el rendimiento comparado con GCD?

Aunque es contraintuitivo, el intercambio innecesario de contexto y el desperdicio de memoria son problemas que se reducen al tener menos hilos.

¿Qué demuestra el ejemplo de código con ThreadingDemonstrator sobre la relación entre tareas e hilos?

private struct ThreadingDemonstrator {
  private func firstTask() async throws {
    print("Task 1 started on thread: \(Thread.currentThread)")
    try await Task.sleep(for: .seconds(2))
    print("Task 1 resumed on thread: \(Thread.currentThread)")
  }

  private func secondTask() async {
    print("Task 2 started on thread: \(Thread.currentThread)")
  }

  func demonstrate() {
    Task {
      try await firstTask()
    }
    Task {
      await secondTask()
    }
  }
}

struct ThreadingDemonstratorTests {
  @Test
  func execute() async throws {
    let sut = ThreadingDemonstrator()
    sut.demonstrate()

    try await Task.sleep(for: .seconds(3))
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. No se puede garantizar el orden de ejecución de las tareas firstTask y secondTask. Para hacerlo hay que usar await.
  2. Cuando se asigna un hilo a una tarea y esta se suspende, no se puede garantizar que lo recupere una vez se reanude.

¿Por qué la tarea 1 puede reanudarse en un hilo diferente al que inició?

Cuando se suspende la tarea 1, cede ("yield") el hilo que tenía y el sistema operativo es libre de asignárselo a cualquier otra tarea.

¿Qué diferencia introduce el modo Swift 6 respecto a acceder a Thread.current y cómo se resuelve?

Usar Thread.current desde un contexto asíncrono arrojará el error de compilación: Class property 'current' is unavailable from asynchronous contexts; Thread.current cannot be used from async contexts..

La solución fue envolver el llamado de Thread.current en un método (variable computada) nonisolated static:

extension Thread {
  public nonisolated static var currentThread: Thread {
    return Thread.current
  }
}
Enter fullscreen mode Exit fullscreen mode

Recitación

Explica con tus propias palabras qué significa "ceder el hilo" (yielding the thread) cuando se usa await.

¿Cuáles son los tres conceptos erróneos sobre Swift Concurrency que menciona el artículo? ¿Puedes refutarlos?

¿Qué tres consecuencias negativas produce el thread explosion en GCD?

¿Cómo describirías el cooperative thread pool a alguien sin experiencia en concurrencia?


Revisión

¿En qué situaciones concretas deberías seguir pensando en si algo corre en el hilo principal o en un hilo en segundo plano?

Si Swift Concurrency ya gestiona los hilos automáticamente, ¿qué valor tiene entender cómo funcionan internamente?

¿Cómo cambiaría tu forma de escribir código asíncrono ahora que sabes que await no bloquea el hilo?

¿Qué preguntas te quedan sin responder sobre el @MainActor y cómo encaja con lo aprendido aquí?


Bibliografía

Top comments (0)