DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Condición de carrera vs. Carrera de datos

1. ¿De qué trata el artículo en términos generales?

"data race", "race condition", el uso de actores para evitar "data race", cómo SC no es capaz de resolver "race condition".

2. ¿Cuáles son los dos conceptos principales que se comparan?

carrera de acceso a datos ("data race") vs condición de carrera ("race condition")

3. ¿Qué es exactamente un data race y bajo qué condiciones ocurre?

Una carrera de acceso a datos ocurre cuando varios hilos tratan de acceder a una porción de memoria compartida sin la sincronización adecuada, donde al menos uno de ellos provoca una mutación.

En el artículo aparece el siguiente ejemplo. Aunque teóricamente entiendo cómo puede fallar, en la práctica no logré obtener una carrera de datos.

nonisolated class DataRaceDemonstrator {

  func demonstrate(_ result: @escaping (Int) -> ()) {
    var counter = 0
    let queue = DispatchQueue.global(qos: .background)

    for _ in 1...10 {
      queue.async {
        // ⚠️ Mutation of captured var 'counter' in
        // concurrently-executing code
        counter += 1
      }
    }

    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
      result(counter)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Por un momento Xcode me mostró la siguiente advertencia que podría ser útil para detectar una carrera de datos:

Mutation of captured var 'counter' in concurrently-executing code

Según el artículo, se puede validar que hay una carrera de datos al activar el "Thread Sanitizer" en las herramientas de diagnóstico en "Edit Schemes".

Thread Sanitizer

Es muy difícil detectar una carrera de datos porque:

  1. Determinar que hace falta sincronización es difícil. Depende de la experiencia del desarrollador.
  2. El código inseguro no necesariamente falla en producción.
  3. Las fallas en runtime son difíciles de reproducir.

Escribí la siguiente prueba automatizada para probar el código de arriba:

struct DataRaceDemonstratorTests {

  @Test
  func exec() async {
    let sut = DataRaceDemonstrator()

    let result = await withCheckedContinuation { continuation in
      sut.demonstrate { result in
        continuation.resume(returning: result)
      }
    }

    #expect(result == 10)
  }
}
Enter fullscreen mode Exit fullscreen mode

La solución que plantea el artículo es cambiar la cola global concurrente por una serial. Sin embargo, todavía tiene la desventaja de que se deben acceder a los datos con serialQueue.async:

// let queue = DispatchQueue.global(qos: .background)
let serialQueue = DispatchQueue(label: "com.example.serial")
// ...
serialQueue.async {
  counter += 1
}
Enter fullscreen mode Exit fullscreen mode

4. ¿Qué es una race condition y en qué se diferencia de un data race?

Un data race ocurre cuando múltiples hilos tratan de acceder a una porción de memoria compartida y al menos uno de ellos está modificando, sin ningún tipo de sincronización. Es un problema de sincronización de acceso.

Un race condition ocurre cuando el resultado del programa depende del orden de ejecución de las tareas y ese orden no se puede garantizar. Es un problema de orden de ejecución.

5. ¿Cómo resuelve Swift Concurrency los data races y cuáles son sus limitaciones?

Para evitar un data race se puede restringir el acceso a los datos por medio de un actor.

El ejemplo anterior se puede cambiar así:

actor Counter {
  private var value = 0
  func increment() {
    value += 1
  }
  func getValue() -> Int {
    value
  }
}
Enter fullscreen mode Exit fullscreen mode
nonisolated struct DataRaceDemonstrator {

  func demonstrate() async -> Int {
    let counter = Counter()
    for _ in 1...10 {
      await counter.increment()
    }

    return await counter.getValue()
  }
}
Enter fullscreen mode Exit fullscreen mode
struct DataRaceDemonstratorTests {

  @Test
  func exec() async {
    let sut = DataRaceDemonstrator()
    let result = await sut.demonstrate()
    #expect(result == 10)
  }
}
Enter fullscreen mode Exit fullscreen mode

Para acceder a los datos del dominio de aislamiento del actor Counter desde cualquier otro dominio de aislamiento, sí o sí se debe usar await.

6. ¿Por qué los data races son difíciles de detectar y reproducir?

Es muy difícil detectar una carrera de datos porque:

  1. Determinar que hace falta sincronización es difícil. Depende de la experiencia del desarrollador.
  2. El código inseguro no necesariamente falla en producción.
  3. Las fallas en runtime son difíciles de reproducir.

7. ¿Qué ventaja ofrece un actor sobre una serial queue de GCD?

Al usar la cola serial de GCD, igual hay que recordar leer y modificar los datos con serialQueue.async, mientras que el actor te obliga a usar await.

8. Según el artículo, ¿por qué el primer ejemplo de código con DispatchQueue.global es problemático aunque parezca correcto?

La operación counter += 1 no es atómica: Implica leer el valor, sumarle uno y escribirlo de nuevo. Como se usa una cola concurrente, entonces múltiples hilos podrían estar ejecutando la operación simultáneamente. Si un hilo está leyendo un valor antes de que otro haya terminado de escribir, se puede corromper el sistema.

Lo más peligroso es que el código no garantiza fallar en tiempo de ejecución, lo que lo hace muy difícil de detectar y reproducir.

9. ¿Por qué la versión con Task dentro del bucle sigue teniendo problemas a pesar de usar un actor?

El actor resuelve el problema de quieen accede al dato, pero no controla cuándo terminan las tareas. Cada Task se lanza de forma independiente y asíncrona. Cuando se ejecuta la línea final counter.getValue() no hay ninguna garantía de que las 10 tareas hayan completado sus incrementos.

El actor cumple su función (no hay dos escrituras simultáneas) pero el orden y la finalización de las tareas queda indefinido.

Recite

  1. ¿Puedes explicar con tus propias palabras la diferencia entre un data race y una race condition?

"data race" es un problema de sincronización de hilos a la hora de acceder a una región de memoria, donde al menos uno está modificándola.

Un "race condition" es un problema que aparece cuando el resultado del programa depende del orden de ejecución de las tareas, y este no puede garantizarse.

  1. ¿Qué garantiza el uso de await al acceder a propiedades de un actor?

Usar await en actor garantiza sincronización en el accedo a un dato. Esto resuelve el problema de "data race", pero no "race condition".

Review

  1. ¿Swift Concurrency resuelve completamente los problemas de concurrencia? ¿Por qué sí o por qué no?

Resuelve el problema de "data race", pero no "race condition".

  1. ¿Qué es el protocolo Sendable y cuál es su relación con los conceptos explicados en el artículo?

Sendable es un tipo de dato que garantiza acceso seguro a su estado. Al usarlo se le dice al compilador que es seguro acceder a estos valores desde cualquier dominio de aislamiento.


Bibliografía

Top comments (0)