c++ multithreading integer thread-safety

Hilo de C++ entero seguro



multithreading integer (6)

A medida que usa GCC, y dependiendo de las operaciones que desee realizar en el entero, puede salirse con las funciones atómicas de GCC .

Estos pueden ser un poco más rápidos que los mutex, pero en algunos casos aún son mucho más lentos que las operaciones "normales".

Actualmente, he creado una clase C ++ para un entero seguro de subprocesos que simplemente almacena un número entero de forma privada y tiene funciones públicas de obtener un conjunto que usan un boost :: mutex para garantizar que solo se pueda aplicar un cambio a la vez al entero.

¿Es esta la forma más eficiente de hacerlo, me han informado que los mutex requieren un gran uso de recursos? La clase se usa mucho, muy rápidamente, por lo que bien podría ser un cuello de botella ...

Google C ++ Thread Safe Integer devuelve vistas y opiniones poco claras sobre la seguridad de subprocesos de operaciones de enteros en diferentes arquitecturas.

Algunos dicen que un int de 32 bits en un arco de 32 bits es seguro, pero 64 sobre 32 no se debe a la "alineación". Otros dicen que es específico del compilador / sistema operativo (lo cual no dudo).

Estoy usando Ubuntu 9.10 en máquinas de 32 bits, algunos tienen núcleos duales y, por lo tanto, los subprocesos pueden ejecutarse simultáneamente en diferentes núcleos en algunos casos y estoy usando el compilador g ++ de GCC 4.4.

Gracias por adelantado...

Nota: la respuesta que he marcado como "correcta" fue la más adecuada para mi problema, sin embargo, hay algunas observaciones excelentes en las otras respuestas y ¡vale la pena leerlas!


C ++ no tiene una implementación de entero atómico real, ni tampoco las bibliotecas más comunes.

Considere el hecho de que incluso si dicha implementación existiera, tendría que depender de algún tipo de exclusión mutua, ya que no puede garantizar operaciones atómicas en todas las arquitecturas.


Existe la biblioteca atómica C ++ 0x, y también hay una biblioteca Boost.Atómica en desarrollo que utiliza técnicas de bloqueo libre.


No es específico del compilador y del sistema operativo, es específico de la arquitectura. El compilador y el sistema operativo entran en él porque son las herramientas con las que trabajas, pero no son las que establecen las reglas reales. Es por esto que el estándar de C ++ no toca el problema.

Nunca en mi vida he oído hablar de una escritura entera de 64 bits, que se puede dividir en dos escrituras de 32 bits, interrumpiéndose a la mitad. (Sí, es una invitación a otros para que publiquen contraejemplos.) Específicamente, nunca he oído hablar de una unidad de carga / almacenamiento de la CPU que permita interrumpir una escritura desalineada; una fuente de interrupción tiene que esperar a que se complete el acceso desalineado completo.

Para tener una unidad de carga / almacenamiento interrumpible, su estado debería guardarse en la pila ... y la unidad de carga / almacenamiento es lo que guarda el resto del estado de la CPU en la pila. Esto sería enormemente complicado, y propenso a errores, si la unidad de carga / almacenamiento fuera interrumpible ... y todo lo que obtendría es un ciclo menos de latencia para responder a las interrupciones, que, en el mejor de los casos, se mide en decenas de ciclos. Totalmente no vale la pena.

En 1997, un compañero de trabajo y yo escribimos una plantilla de cola de C ++ que se usó en un sistema de multiprocesamiento. (Cada procesador tenía su propio sistema operativo en funcionamiento y su propia memoria local, por lo que estas colas solo eran necesarias para la memoria compartida entre los procesadores). Calculamos una manera de hacer que el estado de cambio de cola se hiciera con una sola escritura de entero, y tratamos esta escritura como Una operación atómica. Además, requerimos que cada final de la cola (es decir, el índice de lectura o escritura) sea propiedad de uno y solo un procesador. Trece años después, el código aún funciona bien, e incluso tenemos una versión que maneja múltiples lectores.

Aún así, si desea tratar una escritura de entero de 64 bits como atómica, alinee el campo con un límite de 64 bits. ¿Por que preocuparse?

EDITAR: Para el caso que menciona en su comentario, necesitaría más información para estar seguro, así que déjeme dar un ejemplo de algo que podría implementarse sin un código de sincronización especializado.

Supongamos que tienes N escritores y un lector. Desea que los escritores puedan indicar eventos al lector. Los eventos en sí mismos no tienen datos; solo quieres un conteo de eventos, de verdad.

Declare una estructura para la memoria compartida, compartida entre todos los escritores y el lector:

#include <stdint.h> struct FlagTable { uint32_t flag[NWriters]; };

(Haz de esto una clase o plantilla o lo que sea que creas conveniente).

A cada escritor se le debe indicar su índice y se le debe asignar un puntero a esta tabla:

class Writer {public: Writer(FlagTable* flags_, size_t index_): flags(flags_), index(index_) {} void SignalEvent(uint32_t eventCount = 1); private: FlagTable* flags; size_t index; }

Cuando el escritor quiere señalar un evento (o varios), actualiza su marca:

void Writer::SignalEvent(uint32_t eventCount) { // Effectively atomic: only one writer modifies this value, and // the state changes when the incremented value is written out. flags->flag[index] += eventCount; }

El lector conserva una copia local de todos los valores de bandera que ha visto:

class Reader {public: Reader(FlagTable* flags_): flags(flags_) { for(size_t i = 0; i < NWriters; ++i) seenFlags[i] = flags->flag[i]; } bool AnyEvents(void); uint32_t CountEvents(int writerIndex); private: FlagTable* flags; uint32_t seenFlags[NWriters]; }

Para saber si ha ocurrido algún evento, simplemente busca valores modificados:

bool Reader::AnyEvents(void) { for(size_t i = 0; i < NWriters; ++i) if(seenFlags[i] != flags->flag[i]) return true; return false; }

Si algo sucedió, podemos verificar cada fuente y obtener el recuento de eventos:

uint32_t Reader::CountEvents(int writerIndex) { // Only read a flag once per function call. If you read it twice, // it may change between reads and then funny stuff happens. uint32_t newFlag = flags->flag[i]; // Our local copy, though, we can mess with all we want since there // is only one reader. uint32_t oldFlag = seenFlags[i]; // Next line atomically changes Reader state, marking the events as counted. seenFlags[i] = newFlag; return newFlag - oldFlag; }

Ahora el gran gotcha en todo esto? Es sin bloqueo, lo que quiere decir que no puede hacer que el Reader entre en reposo hasta que un Escritor escriba algo. El lector tiene que elegir entre sentarse en un bucle giratorio esperando que AnyEvents() regrese a ser true , lo que minimiza la latencia, o puede dormir un poco cada vez, lo que ahorra CPU, pero puede permitir que se acumulen muchos eventos. Así que es mejor que nada, pero no es la solución para todo.

Usando primitivas de sincronización reales, solo habría que envolver este código con un mutex y una variable de condición para hacer que se bloquee correctamente: el Reader dormirá hasta que haya algo que hacer. Ya que usó operaciones atómicas con las banderas, realmente podría mantener la cantidad de tiempo que la exclusión está bloqueada al mínimo: el Escritor solo tendría que bloquear la exclusión tanto como para enviar la condición, y no establecer la bandera, y el lector solo necesita esperar la condición antes de llamar a AnyEvents() (básicamente, es como el caso del ciclo de suspensión anterior, pero con una condición de espera de espera en lugar de una llamada de espera).


Para una sincronización completa y de propósito general, como ya han mencionado otros, las herramientas de sincronización tradicionales son bastante necesarias. Sin embargo, para ciertos casos especiales es posible aprovechar las optimizaciones de hardware. Específicamente, la mayoría de las CPU modernas admiten incrementos y decrementos atómicos en enteros. La biblioteca GLib tiene bastante buena compatibilidad multiplataforma para esto. Esencialmente, la biblioteca envuelve el código de ensamblaje específico de la CPU y el compilador para estas operaciones y, por defecto, la protección mutex cuando no están disponibles. Ciertamente no es un propósito muy general, pero si solo está interesado en mantener un contador, esto podría ser suficiente.