2011-08-22 10 views
6

GCC ofrece un bonito conjunto de built-in functions para operaciones atómicas. Y al estar en MacOS o iOS, incluso Apple ofrece un nice set of atomic functions. Sin embargo, todas estas funciones realizan una operación, p. una suma/resta, una operación lógica (AND/OR/XOR) o un compare-and-set/compare-and-swap. Lo que estoy buscando es una manera de asignar atómicamente/leer un valor int, como:Atomically read/write int value sin operación adicional en el valor int en sí

int a; 
/* ... */  
a = someVariable; 

eso es todo. a será leído por otro hilo y solo es importante que a tenga su valor anterior o su valor nuevo. Lamentablemente, el estándar C no garantiza que asignar o leer un valor sea una operación atómica. Recuerdo que una vez leí en alguna parte, que escribir o leer un valor para una variable del tipo int está garantizado que es atómico en GCC (independientemente del tamaño de int) pero busqué en todas partes en la página principal de GCC y no puedo encontrar esta declaración por más tiempo (tal vez fue eliminado).

No puedo usar sig_atomic_t porque sig_atomic_t no tiene un tamaño garantizado y también puede tener un tamaño diferente de int.

Dado que sólo un hilo jamás "escribir" un valor a a, mientras que los dos hilos se "lee" el valor actual de a, no es necesario que realice las operaciones a sí mismos de una manera atómica, por ejemplo:

/* thread 1 */ 
someVariable = atomicRead(a); 
/* Do something with someVariable, non-atomic, when done */ 
atomicWrite(a, someVariable); 

/* thread 2 */ 
someVariable = atomicRead(a); 
/* Do something with someVariable, but never write to a */ 

Si ambos hilos iban a escribir en a, entonces todas las operaciones tendrían que ser atómicas, pero de esa manera, esto solo podría desperdiciar tiempo de CPU; y somos extremadamente bajos en recursos de CPU en nuestro proyecto. Hasta ahora usamos un mutex alrededor de las operaciones de lectura/escritura de a y aunque el mutex se mantiene por tan poco tiempo, esto ya causa problemas (uno de los hilos es un hilo en tiempo real y el bloqueo en un mutex hace que falle sus limitaciones en tiempo real, que es bastante malo).

Por supuesto que podría utilizar un __sync_fetch_and_add para leer la variable (y simplemente añadir "0" a ella, a no modificar su valor) y para la escritura usar un __sync_val_compare_and_swap para escribirlo (que yo sepa su valor anterior, por lo que pasa eso se asegurará de que el valor siempre se intercambie), pero ¿esto no agregará una sobrecarga innecesaria?

Respuesta

3

A __sync_fetch_and_add con un argumento 0 es la mejor opción si quieres que tu carga sea atómica y actúan como una barrera de memoria. De manera similar, puede usar and con 0 o or con -1 para almacenar 0 y -1 atómicamente con una barrera de memoria. Para escribir, puede usar __sync_test_and_set (en realidad una operación xchg) si una barrera "adquirir" es suficiente, o si usa Clang puede usar __sync_swap (que es una operación xchg con una barrera completa).

Sin embargo, en muchos casos es exagerado y es posible que prefiera agregar barreras de memoria manualmente. Si no desea que la barrera de memoria, puede utilizar una carga volátil para leer/atómicamente escribir una variable que está alineado y no más ancha que una palabra:

#define __sync_access(x) (*(volatile __typeof__(x) *) &(x)) 

(Esta macro es un valor-I, lo que también puede Úselo para una tienda como __sync_store(x) = 0). La función implementa la misma semántica que la forma C++ 11 memory_order_consume, pero sólo bajo dos supuestos:

  • que su máquina tiene caches coherentes; de lo contrario, necesita una barrera de memoria o un vaciado de caché global antes de la carga (o antes del primero de un grupo de carga).

  • que su máquina no es un DEC Alpha. El Alpha tenía una semántica muy relajada para reordenar los accesos a la memoria, por lo que necesitaría una barrera de memoria después de cargar (y después de cada carga en un grupo de cargas). En el Alpha, la macro anterior solo proporciona la semántica memory_order_relaxed. Por cierto, las primeras versiones del Alfa ni siquiera podían almacenar un byte atómicamente (solo una palabra, que era de 8 bytes).

En cualquier caso, el __sync_fetch_and_add funcionaría. Hasta donde yo sé, ninguna otra máquina imitaba al Alfa, por lo que ninguna de las suposiciones plantearía problemas en las computadoras actuales.

+0

¿Cómo sé que no es más ancho que una palabra? ¿Pasando por ISO-C-99, que "tipo int" se define para ser siempre tan ancho como palabra? Por lo que puedo decir, esto no está garantizado para ningún tipo int; p.ej. en un microcontrolador de 8 bits, un int aún será de 16 bits, aunque el tamaño de la palabra sea de hecho de 8 bits. – Mecki

+0

Un 'sig_atomic_t' debería funcionar, incluso si está destinado a algo completamente diferente (manejadores de señal). De todos modos, no creo que los backends del microcontrolador implementen '__sync_fetch_and_add', por lo que si los tomas en cuenta todas las apuestas están desactivadas. Se garantiza que –

+1

'intptr_t' y' uintptr_t' typedefs son adecuados para almacenar un puntero, que generalmente es tan ancho como la palabra de máquina. –

2

Las lecturas/escrituras volátiles, alineadas y de tamaño de palabra son atómicas en la mayoría de las plataformas. Verificar su ensamblaje sería la mejor manera de averiguar si esto es cierto en su plataforma. Los registros atómicos no pueden producir casi tantas estructuras interesantes de espera libre como los mecanismos más complicados como comparar y cambiar, por lo que están incluidos.

Ver http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.56.5659&rank=3 para la teoría.

En cuanto a synch_fetch_and_add con un argumento 0, esta parece ser la apuesta más segura. Si le preocupa la eficacia, perfile el código y vea si cumple con sus objetivos de rendimiento. Puede ser víctima de una optimización prematura.