web-dev-qa-db-fra.com

Quel est le verrou mutex de base ou l'entier atomique le plus efficace?

Pour quelque chose de simple comme un compteur si plusieurs threads augmenteront le nombre. J'ai lu que les verrous mutex peuvent diminuer l'efficacité car les threads doivent attendre. Donc, pour moi, un compteur atomique serait le plus efficace, mais j'ai lu qu'en interne, c'est essentiellement un verrou? Je suppose donc que je ne sais pas comment l'un ou l'autre pourrait être plus efficace que l'autre.

46
Matt

Si vous avez un compteur pour lequel les opérations atomiques sont prises en charge, il sera plus efficace qu'un mutex.

Techniquement, l'atomique verrouillera le bus mémoire sur la plupart des plateformes. Cependant, il y a deux détails améliorants:

  • Il est impossible de suspendre un thread pendant le verrouillage du bus mémoire, mais il est possible de suspendre un thread pendant un verrouillage mutex. C'est ce qui vous permet d'obtenir une garantie sans verrouillage (qui ne dit rien sur le non-verrouillage - cela garantit simplement qu'au moins un thread progresse).
  • Les mutex finissent par être implémentés avec des composants atomiques. Étant donné que vous avez besoin d'au moins une opération atomique pour verrouiller un mutex et d'une opération atomique pour déverrouiller un mutex, il faut au moins deux fois de temps pour effectuer un verrouillage mutex, même dans le meilleur des cas.
25
Cort Ammon

Les opérations atomiques exploitent la prise en charge du processeur (comparer et échanger des instructions) et n'utilisent pas du tout de verrous, tandis que les verrous dépendent davantage du système d'exploitation et fonctionnent différemment, par exemple, Win et Linux.

Les verrous suspendent en fait l'exécution du thread, libérant des ressources CPU pour d'autres tâches, mais entraînant des frais généraux de changement de contexte évidents lors de l'arrêt/redémarrage du thread. Au contraire, les threads qui tentent des opérations atomiques n'attendent pas et continuent d'essayer jusqu'au succès (soi-disant occupé), ils n'encourent donc pas de surcharge de changement de contexte, mais ne libèrent pas non plus de ressources CPU.

En résumé, en général, les opérations atomiques sont plus rapides si le conflit entre les threads est suffisamment faible. Vous devriez certainement faire un benchmarking car il n'y a pas d'autre méthode fiable pour savoir quel est le plus bas surcoût entre le changement de contexte et l'attente occupée.

23
yahe

Une mise en œuvre minimale (conforme aux normes) de mutex nécessite 2 ingrédients de base:

  • Un moyen de transmettre atomiquement un changement d'état entre les threads (l'état "verrouillé")
  • barrières de mémoire pour imposer les opérations de mémoire protégées par le mutex pour rester à l'intérieur de la zone protégée.

Il n'y a aucun moyen de le rendre plus simple à cause de la relation "synchronise avec" requise par la norme C++.

Une implémentation minimale (correcte) pourrait ressembler à ceci:

class mutex {
    std::atomic<bool> flag{false};

public:
    void lock()
    {
        while (flag.exchange(true, std::memory_order_relaxed));
        std::atomic_thread_fence(std::memory_order_acquire);
    }

    void unlock()
    {
        std::atomic_thread_fence(std::memory_order_release);
        flag.store(false, std::memory_order_relaxed);
    }
};

En raison de sa simplicité (il ne peut pas suspendre le thread d'exécution), il est probable que, sous faible contention, cette implémentation surpasse un std::mutex. Mais même alors, il est facile de voir que chaque incrément d'entier, protégé par ce mutex, nécessite les opérations suivantes:

  • un magasin atomic pour libérer le mutex
  • un atomic compare-and-swap (lecture-modification-écriture) pour acquérir le mutex (éventuellement plusieurs fois)
  • un incrément entier

Si vous comparez cela avec un std::atomic<int> Autonome qui est incrémenté avec un seul (inconditionnel) lecture-modification-écriture (par exemple fetch_add), Il est raisonnable de s'attendre à ce qu'une opération atomique (en utilisant le même modèle de commande) surpassera le cas où un mutex est utilisé.

9
LWimsey

Les classes de variables atomiques de Java sont capables de profiter des instructions de comparaison et d'échange fournies par le processeur.

Voici une description détaillée des différences: http://www.ibm.com/developerworks/library/j-jtp11234/

6
Ajay

l'entier atomique est un objet mode utilisateur car il est beaucoup plus efficace qu'un mutex qui s'exécute en mode noya. La portée de l'entier atomique est une application unique tandis que la portée du mutex est pour tous les logiciels en cours d'exécution sur la machine.

4
RonTLV

Mutex est une sémantique au niveau du noyau qui fournit une exclusion mutuelle même au Process level. Notez qu'il peut être utile pour étendre l'exclusion mutuelle au-delà des limites du processus et pas seulement au sein d'un processus (pour les threads). C'est plus cher.

Le compteur atomique, AtomicInteger par exemple, est basé sur CAS, et essaie généralement de tenter l'opération jusqu'à ce qu'il réussisse. Fondamentalement, dans ce cas, les threads font la course ou rivalisent pour incrémenter/décrémenter la valeur atomiquement. Ici, vous pouvez voir de bons cycles CPU utilisés par un thread essayant de fonctionner sur une valeur actuelle.

Étant donné que vous souhaitez gérer le compteur, AtomicInteger\AtomicLong sera le meilleur pour votre cas d'utilisation.

1
Sunil Singhal

La plupart des processeurs ont pris en charge une lecture ou une écriture atomique, et souvent un cmp et un échange atomiques. Cela signifie que le processeur lui-même écrit ou lit la dernière valeur en une seule opération, et il peut y avoir quelques cycles perdus par rapport à un accès entier normal, d'autant plus que le compilateur ne peut pas optimiser les opérations atomiques presque aussi bien que la normale.

D'un autre côté, un mutex est un certain nombre de lignes de code à entrer et à quitter, et pendant cette exécution, d'autres processeurs qui accèdent au même emplacement sont totalement bloqués, ce qui représente clairement une surcharge importante pour eux. Dans le code de haut niveau non optimisé, le mutex entrée/sortie et l'atomique seront des appels de fonction, mais pour mutex, tout processeur concurrent sera verrouillé pendant que votre fonction d'entrée mutex revient et que votre fonction de sortie est démarrée. Pour l'atomique, seule la durée de l'opération réelle est verrouillée. L'optimisation devrait réduire ce coût, mais pas tout.

Si vous essayez d'incrémenter, votre processeur moderne prend probablement en charge l'incrémentation/décrémentation atomique, ce qui sera parfait.

Si ce n'est pas le cas, il est soit implémenté à l'aide du processeur atomique cmp & swap, soit à l'aide d'un mutex.

Mutex:

get the lock
read
increment
write
release the lock

Cmp atomique et swap:

atomic read the value
calc the increment
do{
   atomic cmpswap value, increment
   recalc the increment
}while the cmp&swap did not see the expected value

Donc, cette deuxième version a une boucle [dans le cas où un autre processeur incrémente la valeur entre nos opérations atomiques, donc la valeur ne correspond plus, et l'incrément serait faux] qui peut devenir long [s'il y a beaucoup de concurrents], mais devrait généralement être plus rapide que la version mutex, mais la version mutex peut permettre à ce processeur de changer de tâche.

1
Gem Taylor