2010-04-28 21 views
17

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

¿Es esta la forma más eficiente de hacerlo, me han informado que los mutex son bastante intensivos en recursos? La clase se utiliza mucho, muy rápidamente, de modo que bien podría ser un cuello de botella ...

googleing C++ hilo de seguridad Entero vuelve poco claras vistas y opiniones sobre la seguridad de los subprocesos de operaciones con enteros en diferentes arquitecturas.

Algunos dicen que un int 32bit en un arco de 32 bits es seguro, pero 64 en 32 no se debe a 'alineación' Otros dicen que es compilador/OS específico (que no dudo).

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

Gracias de antemano ...

Por favor, tenga en cuenta:La respuesta que he marcado como 'correcta' era el más adecuado para mi problema - sin embargo hay algunos excelentes puntos señalados en las otras respuestas y Son todo vale la pena leer!

+0

, operación atómica puede ser posible, http://stackoverflow.com/questions/930897/c-atomic-operations-for-lock-free -las estructuras pueden ayudar. –

Respuesta

6

No es el compilador y el sistema operativo específico, es específico de la arquitectura. El compilador y el sistema operativo entran en juego porque son las herramientas con las que trabajas, pero no son ellos los que establecen las reglas reales. Esta es la razón por la cual el estándar de C++ no tocará el problema.

Nunca en mi vida he oído hablar de una escritura de enteros de 64 bits, que se puede dividir en dos escrituras de 32 bits, siendo interrumpida a la mitad. (Sí, eso es una invitación para que otros publiquen contraejemplos.) Específicamente, nunca he oído hablar de la unidad de almacenamiento/carga de una CPU que permite que se interrumpa una escritura desalineada; una fuente de interrupción tiene que esperar a que se complete todo el acceso desalineado.

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 latencia al 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 C++ que se utilizó en un sistema de multiprocesamiento. (Cada procesador tenía su propio sistema operativo en ejecución y su propia memoria local, por lo que estas colas solo eran necesarias para la memoria compartida entre procesadores). Buscamos una forma de hacer que la cola cambiara de estado con una sola escritura de enteros, y tratamos esta escritura como una operación atómica. Además, requerimos que cada extremo de la cola (es decir, el índice de lectura o escritura) sea propiedad de un solo procesador. Trece años más tarde, el código sigue funcionando bien, e incluso tenemos una versión que maneja múltiples lectores.

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

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

Supongamos que tiene N escritores y un lector. Desea que los escritores puedan señalar eventos al lector. Los eventos en sí mismos no tienen datos; solo quieres contar un evento, realmente.

Declarar una estructura para la memoria compartida, compartida entre todos los escritores y el lector: (. Establecer como una clase o una plantilla o lo que sea que le parezca)

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

Cada escritor necesita se le dijo que su índice y se le dio 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 indicar un evento (o varios), se actualiza su bandera:

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 mantiene una copia local de todos los valores de los indicadores se 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 averiguar si han ocurrido eventos, sólo se ve por los 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 comprobar 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 problema en todo esto? Es no bloqueante, lo que quiere decir que no puedes hacer que el Reader duerma hasta que un escritor escriba algo. El lector tiene que elegir entre sentarse en un spin-loop esperando AnyEvents() para devolver 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. Entonces es mejor que nada, pero no es la solución para todo.

Utilizando primitivas de sincronización reales, bastaría con envolver este código con un mutex y una variable de condición para bloquearlo correctamente: el Reader dormiría hasta que hubiera algo que hacer. Como usó operaciones atómicas con las banderas, podría mantener la cantidad de tiempo que el mutex está bloqueado al mínimo: el escritor solo necesitaría bloquear el mutex el tiempo suficiente para enviar la condición, y no establecer el indicador, y el lector solo necesita esperar la condición antes de llamar al AnyEvents() (básicamente, es como el caso del bucle de suspensión anterior, pero con una espera de condición en lugar de una llamada de espera).

+1

Actualmente estoy usando entradas de 32 bits pero ¿podría explicarme más sobre lo que quiere decir con "alinear el campo con un límite de 64 bits"? No sé si esto aclara mis necesidades pero: hay múltiples hilos escribiendo (incrementando) y un lector. –

+0

Si tiene un entero de 64 bits, entonces desea alinearlo con un límite de 64 bits, es decir, una dirección de bytes uniformemente divisible por 'sizeof (uint64_t)' (o lo que sea que intente alinear). Consulte http://en.wikipedia.org/wiki/Data_structure_alignment para obtener más detalles. –

+0

Dulce ... Expandí mi respuesta e hice downvoted sin explicación. –

4

C++ no tiene una implementación real de enteros atómicos, como tampoco lo hacen las bibliotecas más comunes.

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

7

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

4

Como está utilizando GCC, y dependiendo de las operaciones que desee realizar en el entero, puede salirse con GCC's atomic builtins.

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

2

Para una sincronización completa y de propósito general, como ya se ha mencionado, 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 el incremento atómico & en números enteros. La biblioteca GLib tiene una buena compatibilidad multiplataforma para esto. Básicamente, la biblioteca ajusta el código de ensamblado específico del compilador de la CPU & para estas operaciones y se predetermina a la protección de exclusión mutua donde no están disponibles. Ciertamente, no es muy general, pero si solo está interesado en mantener un contador, esto podría ser suficiente.

Cuestiones relacionadas