threads lock intrinsic multithreading synchronization thread-safety locking

multithreading - lock - synchronized android



Primitivas primitivas de sincronización-¿seguro? (5)

En dispositivos restringidos, a menudo me encuentro "falsificando" bloqueos entre 2 hilos con 2 bools. Cada uno solo es leído por un hilo, y solo escrito por el otro. Esto es lo que quiero decir:

bool quitted = false, paused = false; bool should_quit = false, should_pause = false; void downloader_thread() { quitted = false; while(!should_quit) { fill_buffer(bfr); if(should_pause) { is_paused = true; while(should_pause) sleep(50); is_paused = false; } } quitted = true; } void ui_thread() { // new Thread(downloader_thread).start(); // ... should_pause = true; while(!is_paused) sleep(50); // resize buffer or something else non-thread-safe should_pause = false; }

Por supuesto, en una PC no haría esto, pero en dispositivos restringidos, parece que leer un valor bool sería mucho más rápido que obtener un bloqueo. Por supuesto, me cambio para una recuperación más lenta (ver " sleep(50) ") cuando se necesita un cambio en el buffer.

La pregunta es: ¿es completamente seguro para subprocesos? ¿O hay fallas ocultas que debo tener en cuenta cuando falsifiquemos bloqueos como este? ¿O no debería hacer esto?


A menos que comprenda en detalle la arquitectura de memoria de su dispositivo, así como también el código generado por su compilador, este código no es seguro.

Solo porque parece que funcionaría, no significa que lo hará. Los dispositivos "restringidos", como el tipo sin restricciones, son cada vez más potentes. No apostaría en contra de encontrar una CPU de doble núcleo en un teléfono celular, por ejemplo. Eso significa que no apostaría a que el código anterior funcionaría.


Con respecto a la llamada de espera, siempre puedes simplemente dormir (0) o la llamada equivalente que pausa tu hilo, dejando que el siguiente en la línea un turno.

En cuanto al resto, esto es seguro para hilos si conoce los detalles de implementación de su dispositivo.


El uso de valores de bool para comunicarse entre hilos puede funcionar como usted desea, pero en realidad hay dos errores ocultos como se explica en esta publicación de blog por Vitaliy Liptchinsky :

Coherencia de caché

Una CPU no siempre recupera los valores de memoria de la RAM. Los cachés rápidos de memoria en el dado son uno de los trucos utilizados por los diseñadores de CPU para evitar el cuello de botella de Von Neumann . En algunas arquitecturas multi-cpu o multi-core (como Itanium de Intel ) estas cachés de CPU no se comparten ni se sincronizan automáticamente. En otras palabras, sus hilos pueden estar viendo valores diferentes para la misma dirección de memoria si se ejecutan en diferentes CPU.

Para evitar esto, necesita declarar sus variables como volátiles ( C ++ , C # , java ) o hacer lecturas / escrituras volátiles explícitas , o hacer uso de mecanismos de bloqueo.

Optimizaciones del compilador

El compilador o JITter puede realizar optimizaciones que no son seguras si se trata de varios subprocesos. Consulte la publicación de blog vinculada para ver un ejemplo. Nuevamente, debe hacer uso de la palabra clave volátil u otros mecanismos para informarle al compilador.


Respondiendo las preguntas

¿Es esto completamente seguro para subprocesos? Yo respondería que no, esto no es seguro para subprocesos y que simplemente no haría esto en absoluto. Sin conocer los detalles de nuestro dispositivo y compilador, si se trata de C ++, el compilador puede reordenar y optimizar las cosas como lo considere oportuno. por ejemplo, usted escribió:

is_paused = true; while(should_pause) sleep(50); is_paused = false;

pero el compilador puede elegir reordenar esto en algo como esto:

sleep(50); is_paused = false;

esto probablemente no funcionará ni siquiera con un solo dispositivo central como otros han dicho.

En lugar de bloquear, puede intentar hacer mejor para hacer menos en el subproceso de interfaz de usuario en lugar de ceder en el medio del procesamiento de los mensajes de la interfaz de usuario. Si crees que has gastado demasiado tiempo en el hilo de la interfaz de usuario, entonces encuentra una manera de salir limpiamente y registrar una llamada asincrónica.

Si llama a suspensión en un subproceso de interfaz de usuario (o intenta obtener un bloqueo o hacer algo que pueda bloquear) abre la puerta a bloqueos y IU con problemas. Un sueño de 50ms es suficiente para que el usuario lo note. Y si intentas adquirir un bloqueo o realizar cualquier otra operación de bloqueo (como E / S), tienes que lidiar con la realidad de esperar un tiempo indeterminado para que la E / S, que tiende a traducirse del error, se cuelgue.


Este código no es seguro en casi todas las circunstancias. En los procesadores multinúcleo, no tendrá coherencia de caché entre los núcleos porque las lecturas y escrituras de bool no son operaciones atómicas. Esto significa que no se garantiza que cada núcleo tenga el mismo valor en la memoria caché o incluso en la memoria si la caché de la última escritura no se ha eliminado.

Sin embargo, incluso en dispositivos de núcleo único con recursos limitados esto no es seguro porque no tiene control sobre el programador. Aquí hay un ejemplo, por simplicidad voy a pretender que estos son los únicos dos hilos en el dispositivo.

Cuando se ejecuta ui_thread, las siguientes líneas de código podrían ejecutarse en el mismo timeslice.

// new Thread(downloader_thread).start(); // ... should_pause = true;

El downloader_thread se ejecuta al lado y en su segmento de tiempo se ejecutan las siguientes líneas:

quitted = false; while(!should_quit) { fill_buffer(bfr);

El planificador realiza el downloader_thread antes de que fill_buffer regrese y luego activa el ui_thread que se ejecuta.

while(!is_paused) sleep(50); // resize buffer or something else non-thread-safe should_pause = false;

La operación de cambio de tamaño del búfer se realiza mientras el hilo de descarga está en el proceso de llenar el búfer. Esto significa que el búfer está dañado y es probable que se cuelgue pronto. No sucederá siempre, pero el hecho de que estés llenando el búfer antes de establecer is_paused en true hace que sea más probable que suceda, pero incluso si cambiaras el orden de esas dos operaciones en el downloader_thread, aún tendrías una condición de carrera. , pero probablemente te encuentres en un punto muerto en lugar de corromper el buffer.

Por cierto, este es un tipo de spinlock, simplemente no funciona. Los Spinlock no son muy buenos para tiempos de espera que probablemente se extiendan a muchas porciones de tiempo, ya que hacen girar el procesador. Su implementación duerme, que es un poco más agradable, pero el planificador todavía tiene que ejecutar su hilo y los cambios de contexto de hilo no son baratos. Si está esperando en una sección crítica o semáforo, el planificador no activará su subproceso nuevamente hasta que el recurso se haya liberado.

Es posible que pueda salirse con la suya de alguna forma en una plataforma / arquitectura específica, pero es muy fácil cometer un error que es muy difícil de rastrear.