2010-11-12 16 views
11

Debo estar solo teniendo un momento, porque esto debería ser fácil, pero parece que no puedo hacerlo funcionar bien.Contador atómico en gcc

¿Cuál es la forma correcta de implementar un contador atómico en GCC?

es decir, quiero un contador que se ejecuta de cero a 4 y es seguro para subprocesos.

yo estaba haciendo esto (que se envuelve más en una clase, pero no aquí)

static volatile int _count = 0; 
const int limit = 4; 

int get_count(){ 
    // Create a local copy of diskid 
    int save_count = __sync_fetch_and_add(&_count, 1); 
    if (save_count >= limit){ 
     __sync_fetch_and_and(&_count, 0); // Set it back to zero 
    } 
    return save_count; 
} 

Pero se está ejecutando desde 1 a partir de 1 - 4 inclusive luego alrededor de cero.
Debería ir de 0 a 3. Normalmente haría un contador con un operador de mod pero no sé cómo hacerlo de manera segura.

Quizás esta versión sea mejor. ¿Puedes ver algún problema con él, u ofrecer una mejor solución?

int get_count(){ 
    // Create a local copy of diskid 
    int save_count = _count; 
    if (save_count >= limit){ 
     __sync_fetch_and_and(&_count, 0); // Set it back to zero 
     return 0; 
    } 

    return save_count; 
} 

En realidad, debo señalar que no es absolutamente crítico que cada hilo tenga un valor diferente. Si dos hilos pasaron a leer el mismo valor al mismo tiempo, eso no sería un problema. Pero no pueden exceder el límite en ningún momento.

Respuesta

13

¡Su código no es atómico (y su segundo get_count ni siquiera incrementa el valor del contador)!

Diga count es 3 en el inicio y dos hilos simultáneamente llaman get_count. Uno de ellos obtendrá su complemento atómico primero e incrementará count a 4. Si el segundo subproceso es lo suficientemente rápido, puede incrementarlo a 5 antes de que el primer subproceso lo restablezca a cero.

Además, en su procesamiento envolvente, restablece count a 0 pero no a save_count. Esto claramente no es lo que se pretende.

Esto es más fácil si limit es una potencia de 2. No vuelvas a hacer la reducción a sí mismo, sólo tiene que utilizar

return (unsigned) __sync_fetch_and_add(&count, 1) % (unsigned) limit; 

o alternativamente

return __sync_fetch_and_add(&count, 1) & (limit - 1); 

Esto sólo hace una operación atómica por la invocación , es seguro y muy barato. Para los límites genéricos, aún puede usar %, pero eso romperá la secuencia si el contador se desborda alguna vez. Puedes intentar usar un valor de 64 bits (si tu plataforma admite atómicos de 64 bits) y solo espero que nunca se desborde; esta es una mala idea sin embargo. La forma correcta de hacerlo es usar una operación de intercambio de comparación atómica. Esto se hace:

int old_count, new_count; 
do { 
    old_count = count; 
    new_count = old_count + 1; 
    if (new_count >= limit) new_count = 0; // or use % 
} while (!__sync_bool_compare_and_swap(&count, old_count, new_count)); 

Este enfoque se generaliza a secuencias más complicadas y las operaciones de actualización también. Dicho esto, este tipo de operación sin cerradura es complicado de acertar, se basa en un comportamiento indefinido hasta cierto punto (todos los compiladores actuales lo hacen bien, pero no C/C++ estándar antes de que C++ 0x realmente tenga una definición modelo de memoria) y es fácil de romper. Recomiendo usar un mutex/lock simple a menos que lo hayas perfilado y lo encuentres como un cuello de botella.

+0

Si __sync_fetch_and_add "realiza una operación atómica por invocación" depende de la CPU, no especificada en la pregunta. Puede ser implementado según su enfoque de comparar y cambiar, que es lo que he usado en el hardware de Sun en el pasado (bueno, la implementación de mi ex colega, muy conocida como "atomic_robin" :-)). –

+0

No estaba hablando del número de instrucciones ejecutadas; Hay diferentes formas de implementar Exchange-add, pero todas son equivalentes siempre y cuando solo escriban en la memoria ("commit") una vez. El punto es que no se pueden construir primitivas atómicas "grandes" a partir de varias pequeñas; ellos no componen Puede usar varios pasos, pero el paso final (confirmar) debe ser una operación atómica única que hace que todo sea visible. Si hay más de un paso de este tipo al final, automáticamente tendrá una condición de carrera. –

+0

Hola, gracias. Esto es exactamente lo que estoy buscando. No sé exactamente lo que estaba pensando con mi segunda solución. Creo que fue la falta de sueño lo que me hizo escribir un código tan bueno. – Matt

0

Es imposible crear nada atómico en C puro, incluso con volatile. Necesitas asm. C1x tendrá tipos atómicos especiales, pero hasta entonces estás atascado con asm.

+0

Lo siento, no debería haber etiquetado como C. Esto es C++, sin embargo, no utiliza c1x. – Matt

+3

Pure C, claro, pero también está etiquetado con gcc, por lo que los intrinsics/builtins son la mejor opción. –

+1

De hecho, mi mal. –

0

Tiene dos problemas.

__sync_fetch_and_add devolverá el valor anterior (es decir, antes de añadir uno). Por lo tanto, en el paso donde _count se convierte en 3, su variable local save_count está volviendo 2. Entonces, realmente tiene que incrementar _count hasta 4 antes de que regrese como 3.

Pero además de eso, usted está buscando específicamente que sea >= 4 antes de restablecerlo de nuevo a 0. Esa es solo una cuestión de usar el límite incorrecto si solo está buscando que sea tan alto como tres.

2

Tiene suerte, porque el rango que desea encaja exactamente en 2 bits.

Solución fácil: deje que la variable volátil cuente para siempre. Pero después de leerlo, use solo los dos bits más bajos (val & 3). Presto, contador atómico de 0-3.

+0

o simplemente use mod – Inverse

+0

@Inverso: los mods pueden ser mucho más lentos ... y es una apuesta más segura. –

+0

Ese es un buen punto. Aunque necesito que el valor del límite sea arbitrario. Viene de un archivo de configuración. – Matt