web-dev-qa-db-fra.com

C ++ 11 Implémentation de Spinlock en utilisant <atomic>

J'ai implémenté la classe SpinLock, comme suit

struct Node {
    int number;
    std::atomic_bool latch;

    void add() {
        lock();
        number++;
        unlock();
    }
    void lock() {
        bool unlatched = false;
        while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
    }
    void unlock() {
        latch.store(false , std::memory_order_release);
    }
};

J'ai implémenté la classe ci-dessus et créé deux threads qui appellent la méthode add () d'une même instance de la classe Node 10 millions de fois par thread.

le résultat n'est malheureusement pas de 20 millions. Qu'est-ce que j'oublie ici?

31
syko

Le problème est que compare_exchange_weak met à jour la variable unlatched en cas d'échec. D'après la documentation de compare_exchange_weak:

Compare le contenu de la valeur contenue de l'objet atomique avec la valeur attendue: - si vrai, il remplace la valeur contenue par val (comme store). - si faux, il remplace attendu par la valeur contenue.

C'est-à-dire, après le premier échec compare_exchange_weak, unlatched sera mis à jour vers true, donc la prochaine itération de la boucle essaiera de compare_exchange_weaktrue avec true. Cela réussit et vous venez de prendre un verrou qui était détenu par un autre thread.

Solution: assurez-vous de remettre unlatched sur false avant chaque compare_exchange_weak, par exemple.:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}
40
gexicide

Comme mentionné par @gexicide, le problème est que les fonctions compare_exchange Mettent à jour la variable expected avec la valeur actuelle de la variable atomique. C'est aussi la raison pour laquelle vous devez utiliser la variable locale unlatched en premier lieu. Pour résoudre ce problème, vous pouvez redéfinir unlatched sur false dans chaque itération de boucle.

Cependant, au lieu d'utiliser compare_exchange Pour quelque chose que son interface est plutôt mal adaptée, il est beaucoup plus simple d'utiliser std::atomic_flag À la place:

class SpinLock {
    std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { ; }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};

Source: cppreference

Spécifier manuellement l'ordre de la mémoire n'est qu'un Tweak de performance potentielle mineur, que j'ai copié à partir de la source. Si la simplicité est plus importante que le dernier bit de performance, vous pouvez vous en tenir aux valeurs par défaut et appeler simplement locked.test_and_set() / locked.clear().

Btw .: std::atomic_flag Est le seul type qui est garanti sans verrouillage, bien que je ne connaisse aucune plate-forme, où les opérations sur std::atomic_bool Ne sont pas sans verrouillage.

Mise à jour: Comme expliqué dans les commentaires de @David Schwartz, @Anton et @Technik Empire, la boucle vide a des effets indésirables comme la mauvaise prédiction de branche, la famine de threads sur les processeurs HT et une consommation d'énergie trop élevée - donc en bref, c'est une façon assez inefficace d'attendre. L'impact et la solution sont spécifiques à l'architecture, à la plate-forme et à l'application. Je ne suis pas un expert, mais la solution habituelle semble être d'ajouter une cpu_relax() sur linux ou YieldProcessor() sur windows au corps de la boucle.

EDIT2: Juste pour être clair: la version portable présentée ici (sans les instructions spéciales cpu_relax etc.) devrait déjà être assez bonne pour de nombreuses applications. Si votre SpinLock tourne beaucoup parce que quelqu'un d'autre tient le verrou depuis longtemps (ce qui pourrait déjà indiquer un problème de conception général), il est probablement préférable d'utiliser un mutex normal de toute façon.

33
MikeMB