2010-11-12 10 views
11

Je dois avoir juste un moment, car cela devrait être facile mais je n'arrive pas à le faire fonctionner correctement.Compteur atomique en gcc

Quelle est la bonne façon d'implémenter un compteur atomique dans GCC?

C'est-à-dire que je veux un compteur qui va de zéro à 4 et qui soit sûr pour les threads.

que je faisais ce (qui est encore enveloppé dans une classe, mais pas ici)

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; 
} 

Mais il fonctionne de 1 à 1 à 4 inclus puis autour de zéro.
Il devrait aller de 0 à 3. Normalement, je ferais un compteur avec un opérateur de mod, mais je ne sais pas comment le faire en toute sécurité.

Peut-être que cette version est meilleure. Pouvez-vous voir des problèmes avec cela, ou offrir une meilleure solution.

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 fait, je dois souligner qu'il n'est pas absolument essentiel que chaque thread obtienne une valeur différente. Si deux threads arrivaient à lire la même valeur en même temps, cela ne poserait pas de problème. Mais ils ne peuvent pas dépasser la limite à tout moment.

Répondre

13

Votre code n'est pas atomique (et votre deuxième get_count n'augmente même pas la valeur du compteur)!

Dites count est 3 au début et deux threads simultanément appeler get_count. L'un d'entre eux obtiendra son ajout atomique en premier et incrémentera count à 4. Si le second thread est assez rapide, il peut l'incrémenter à 5 avant que le premier thread ne le remette à zéro.

En outre, dans votre traitement de bouclage, vous réinitialisez count à 0 mais pas save_count. Ce n'est clairement pas ce qui est prévu.

C'est plus facile si limit est une puissance de 2. Ne jamais faire la réduction vous, il suffit d'utiliser

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

ou bien

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

Cela ne fait qu'une seule opération atomique par appel , est sûr et très bon marché. Pour les limites génériques, vous pouvez toujours utiliser %, mais cela interrompra la séquence si le compteur déborde toujours. Vous pouvez essayer d'utiliser une valeur 64 bits (si votre plate-forme prend en charge les atomiques 64 bits) et espérer qu'il ne déborde jamais; C'est une mauvaise idée cependant. La bonne façon de procéder est d'utiliser une opération de comparaison atomique atomique. Vous faites ceci:

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)); 

Cette approche se généralise à des séquences plus compliquées et à des opérations de mise à jour également. Cela dit, ce type d'opération sans clé est difficile à obtenir, repose sur un comportement indéfini dans une certaine mesure (tous les compilateurs actuels ont raison, mais aucun standard C/C++ avant que C++ 0x n'ait une définition modèle de mémoire) et est facile à casser. Je recommande d'utiliser un simple mutex/lock à moins que vous ne l'ayez profilé et trouvé que cela soit un goulot d'étranglement.

+0

Si __sync_fetch_and_add "effectue une opération atomique par invocation" dépend du processeur - non spécifié dans la question. Il pourrait bien être implémenté selon votre approche compare-and-swap, qui est ce que j'ai utilisé sur le matériel Sun dans le passé (enfin, l'implémentation de mon ex-collègue, nommée "atomic_robin" :-)). –

+0

Je ne parlais pas du nombre d'instructions exécutées; Il existe différentes manières d'implémenter exchange-add, mais elles sont toutes équivalentes tant qu'elles n'écrivent réellement en mémoire ("commit") qu'une seule fois. Le fait est que vous ne pouvez pas construire de "grandes" primitives atomiques parmi plusieurs petites; ils ne composent pas. Vous pouvez utiliser plusieurs étapes, mais l'étape finale (validation) doit être une opération atomique unique qui rend tout visible. S'il y a plus d'une telle étape à la fin, vous avez automatiquement une condition de compétition. –

+0

Salut merci. C'est exactement ce que je cherche. Je ne sais pas tout à fait ce que je pensais avec ma deuxième solution. Je suppose que c'était le manque de sommeil qui m'a fait écrire un si bon code. – Matt

0

Il est impossible de créer quoi que ce soit d'atomique en C pur, même avec volatile. Tu as besoin d'asm. C1x aura des types atomiques spéciaux, mais jusque là vous êtes coincé avec asm.

+0

Désolé je ne devrais pas l'avoir étiqueté comme C. C'est C++, n'utilisant pas c1x si. – Matt

+3

Pure C, bien sûr, mais il a aussi marqué gcc, donc les intrinsèques/builtins sont la meilleure option. –

+1

En effet, mon mauvais. –

0

Vous avez deux problèmes.

__sync_fetch_and_add renverra le précédente valeur (à savoir, avant d'ajouter une). Donc, à l'étape où _count devient 3, votre variable locale save_count récupère 2. Vous devez donc incrémenter _count jusqu'à 4 avant de revenir en tant que 3.

Mais même en plus de cela, vous le recherchez spécifiquement >= 4 avant de le remettre à 0. C'est juste une question d'utiliser la mauvaise limite si vous cherchez seulement à être aussi haut comme trois.

2

Vous avez de la chance, parce que la gamme que vous voulez arrive à tenir dans exactement 2 bits. Solution facile: Laisser la variable volatile compter indéfiniment. Mais après l'avoir lu, utilisez seulement les deux bits les plus bas (val & 3). Presto, compteur atomique de 0-3.

+0

ou simplement utiliser le mod – Inverse

+0

@Inverse: mod peut être beaucoup plus lent ... et est un pari plus sûr. –

+0

C'est un bon point. Bien que j'ai besoin de la valeur de limite pour être arbitraire. Cela vient d'un fichier de configuration. – Matt