¿Debo agregar el locking de subprocesss a variables simples?

Digamos que tengo un object para el cual varios subprocesss pueden leer / escribir en el state y someValue variables de valor. ¿Debo agregar locking si estas variables son types como int, double, enums, etc.?

 enum State: String { case one case two } class Object { var state: State var someValue: Double } 

Si tu puedes.

Imagine la situación en la que dos subprocesss intentan agregar 1 a someValue . Un hilo hace esto por:

  1. lea someValue en un logging
  2. Añadir 1
  3. escribe algo de someValue

Si ambos subprocesss hacen la operación 1 antes de la operación 3, obtendrán una respuesta diferente que si un subprocess realiza las tres operaciones antes de que la otra subprocess realice la operación 1.

También hay problemas más sutiles, ya que un comstackdor de optimization podría no volver a escribir el valor modificado del logging durante un time, si es que lo hizo. Además, las CPU modernas tienen múltiples núcleos cada uno con su propio caching. La CPU que escribe un valor en la memory no garantiza que llegue a la memory de inmediato. Puede llegar tan lejos como el caching del núcleo. Necesitas lo que se llama una barrera de memory para asegurarte de que todo vuelva a estar escrito en la memory principal.

En la escala más grande, necesitará locking para garantizar la coinheritance entre las variables de su class. Entonces, si el estado está destinado a representar alguna propiedad de someValue por ejemplo, es un número integer o no, necesitará bloquear para garantizar que todos tengan siempre una visión coherente, es decir

  1. modificar someValue
  2. probar el nuevo valor
  3. establezca el state correspondiente.

Las tres operaciones anteriores deben parecer atómicas, o si el object se examina después de la operación 1 pero antes de la operación 3, estará en un estado incoherente.

"Necesita bloquearse" debe ser calificado con lo que espera estar seguro. Si necesita actualizar más de un valor de manera coordinada, ciertamente necesita bloquear. Si haces una lectura / modificación / escritura en más de un hilo, necesitas bloquear o usar un código especial especulativo que pueda notar la interrupción de otro hilo. Para el uso simple de valores únicos, puede usar operaciones atómicas especiales. A veces, simplemente establecer un valor no necesita locking, pero eso depende de la situación.

Lo que JeremyP dice, pero también debe considerar los niveles más altos: su "estado" y "someValue" podrían estar relacionados. Así que si cambio el estado, entonces someValue, el contenido del object integer justo después de cambiar "state" puede ser basura porque el nuevo estado no coincide con the old someValue.

Las soluciones simples están buscando en Google cómo hacer "@synchronized" en Swift, o enviarlo al hilo principal, o enviarlo a una queue de serie.

Con el fin de simular su problema, rastré el siguiente fragment de código (entorno de la aplicación iOS):

 import UIKit func delay ( _ seconds: Double, queue: DispatchQueue = DispatchQueue.main, after: @escaping ()->()) { let time = DispatchTime.now() + Double(Int64(seconds * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) queue.asyncAfter(deadline: time, execute: after) } class ViewController: UIViewController { var myValue = 0 override func viewDidLoad() { super.viewDidLoad() addOneThousand() addOneThousand() addOneThousand() // calling this is just for logging the value after a delay // just for making sure that all threads execution is completed... delay(3.0) { print(self.myValue) } } func addOneThousand() { DispatchQueue(label: "com.myapp.myqueue").async { for _ in 0...999 { self.myValue += 1 } print(self.myValue) } } } 

Para la primera vista, la expectativa sería: el valor de myValue debería ser de 3000, ya que addOneThousand() se ha llamado tres veces, pero después de ejecutar la aplicación 10 veces, en mi máquina (simulador), secuencialmente, la salida fue:

1582 1582 1582 1582

3000 3000 3000 3000

2523 2523 2523 2523

2591 2591 2591 2591

1689 1689 1689 1689

1556 1556 1556 1556

1991 1991 1991 1991

1914 1914 1914 1914

2416 2416 2416 2416

1889 1889 1889 1889

Lo más importante es que el cuarto valor para cada resultado (la salida después de esperar el retraso) es la mayoría de las veces es inesperado (no 3000). Si no estoy confundiendo, asumo que lo que estamos enfrentando aquí es una condición de carrera .


Una solución adecuada para tal caso es permitir que la ejecución de los hilos sea serializada ; después de editar addOneThousand() ( sync lugar de async . Es posible que desee verificar esta respuesta ):

 func addOneThousand() { DispatchQueue(label: "com.myapp.myqueue").sync { for _ in 0...999 { self.myValue += 1 } print(self.myValue) } } 

el resultado para 10 series secuenciales fue:

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

1000 2000 3000 3000

Eso representa el resultado esperado.

Espero que sea de ayuda.

Estrictamente hablando, no (aunque "sí" también sería una respuesta válida, y podría decirse que es una respuesta más correcta para el caso general).

Dependiendo de lo que necesite / desee, es posible que simplemente use operaciones atómicas utilizando funciones como, por ejemplo, OSAtomicIncrement32 o OSAtomicCompareAndSwapPtr que no está bloqueado .

Tenga en count, sin embargo, que incluso si una operación es atómica, dos operaciones atómicas individualmente consecutivas no son atómicas en su totalidad. Por lo tanto, si, por ejemplo, desea actualizar constantemente el state y el valor de someValue , ir sin un locking es totalmente imposible si la corrección importa (a less que, por coincidencia, sean lo suficientemente pequeños para que pueda engañarlos y exprimirlos en un solo átomo más grande tipo).

También tenga en count que, aunque necesite bloquear o usar operaciones atómicas para corregir el progtwig , ocasionalmente puede "escaping" ocasionalmente sin hacerlo. Esto se debe a que en la mayoría de las plataforms, las cargas y los almacenes ordinarios para las direcciones de memory correctamente alineadas son atómicas de todos modos.
Sin embargo, no se sienta tentado, no es tan bueno como parece y, de hecho, no es nada bueno, confiar en que las cosas funcionarán (incluso si "probó" y funciona bien) crea la tipo de progtwig que funciona bien durante el desarrollo y luego provoca mil tickets de soporte un mes después del envío, y no hay una indicación obvia de lo que salió mal.