66 votes

L'opérateur ++ est-il sans danger pour les fils ?

NOTE : Je ne suis vraiment pas très doué pour la programmation multithread, mais mon projet actuel m'oblige à le faire et j'essaie de comprendre ce qui est thread safe et ce qui ne l'est pas.

Je lisais un des livres d'Eric Lippert. des réponses impressionnantes sur ce que ++i fait . Il dit que c'est ce qui se passe réellement :

  1. x est évalué pour produire la variable
  2. la valeur de la variable est copiée dans un emplacement temporaire
  3. la valeur temporaire est incrémentée pour produire une nouvelle valeur (sans écraser la valeur temporaire !).
  4. la nouvelle valeur est stockée dans la variable
  5. le résultat de l'opération est la nouvelle valeur

Cela m'a fait réfléchir, et si deux threads appelaient ++i ? Si le premier thread est à l'étape 3 alors que le second thread est à l'étape 2 (c'est-à-dire si le second thread copie la valeur à l'emplacement temporaire avant que le premier thread n'enregistre la nouvelle valeur dans la variable) ?

Si cela se produit, il semblerait que les deux fils s'incrémentent seulement i une fois au lieu de deux. (A moins que le tout soit dans une lock .)

91voto

Eric Lippert Points 300275

Comme d'autres réponses l'ont souligné, non, ++ n'est pas "threadsafe".

Je pense qu'il est utile, à mesure que vous apprenez à connaître le multithreading et ses dangers, de commencer à être très précis sur ce que vous entendez par "threadsafe", car différentes personnes entendent différentes choses par là. Essentiellement, l'aspect de la threadsafe qui vous intéresse ici est de savoir si l'opération est atomique ou non. Une opération "atomique" est une opération dont il est garanti qu'elle n'est pas à moitié terminée lorsqu'elle est interrompue par un autre thread.

(Il y a beaucoup d'autres problèmes de threading qui n'ont rien à voir avec l'atomicité mais qui peuvent tout de même correspondre aux définitions de la sécurité des threads de certaines personnes. Par exemple, étant donné que deux threads mutent chacun une variable, et que deux threads lisent chacun la variable, les deux lecteurs sont-ils garantis de se mettre d'accord sur la variable commander dans lequel les deux autres fils ont fait des mutations ? Si votre logique dépend de cela, alors vous avez un problème de thread safety très difficile à gérer, même si chaque lecture et écriture est atomique).

En C#, pratiquement rien n'est garanti comme étant atomique. Brièvement :

  • lecture d'un entier ou d'un flottant de 32 bits
  • lecture d'une référence
  • écriture d'un entier de 32 bits ou d'un flottant
  • rédiger une référence

sont garanties comme étant atomiques (voir la spécification pour les détails exacts).

En particulier, la lecture et l'écriture d'un entier ou d'un flottant de 64 bits est no garantie d'être atomique. Si vous dites :

C.x = 0xDEADBEEF00000000;

sur un fil, et

C.x = 0x000000000BADF00D;

sur un autre fil, alors il est possible de le faire sur un troisième fil :

Console.WriteLine(C.x);

faire en sorte que cela écrive 0xDEADBEEF0BADF00D, même si logiquement la variable n'a jamais eu cette valeur. Le langage C# se réserve le droit de faire en sorte que l'écriture dans un long soit équivalente à l'écriture dans deux ints, l'un après l'autre, et en pratique, certaines puces l'implémentent de cette façon. Un changement de thread après la première des écritures peut amener un lecteur à lire quelque chose d'inattendu.

En bref, ne partagez pas tout ce qui est entre deux fils sans verrouiller quelque chose. Les verrous ne sont lents que lorsqu'ils sont satisfaits ; si vous avez un problème de performance à cause de verrous contestés, alors réparer le défaut d'architecture qui conduit à des serrures contestées . Si les verrous ne sont pas contestés et sont encore trop lents, ce n'est qu'à ce moment-là que vous devez envisager de passer à des techniques dangereuses de verrouillage bas.

La technique courante de verrouillage faible à utiliser ici est bien sûr d'appeler Threading.Interlocked.Increment qui effectue l'incrémentation d'un entier d'une manière garantie comme étant atomique. (Notez cependant qu'il n'offre toujours pas de garanties sur des choses comme ce qui se passe si deux threads effectuent chacun des incréments imbriqués de deux variables différentes à des moments différents, et que d'autres threads essaient de déterminer quel incrément s'est produit "en premier". C# ne garantit pas qu'un ordre unique et cohérent des événements soit vu par tous les threads).

36voto

Oded Points 271275

Non, ça ne l'est pas.

L'analyse à laquelle vous avez abouti est tout à fait correcte. ++ (et -- ) sont vulnérables aux conditions de course s'ils ne sont pas utilisés avec une sémantique de verrouillage appropriée.

Il est difficile de les réaliser correctement, mais heureusement, le BCL a une solution toute prête pour ces cas spécifiques :

Vous devez utiliser Interlocked.Increment si vous voulez une opération d'incrémentation atomique. Il existe également un Decrement et plusieurs autres opérations atomiques utiles définies sur l'élément Interlocked classe.

Incrémente une variable spécifiée et stocke le résultat, comme une opération atomique.

12voto

Henk Holterman Points 153608

Non, i++ a l'air compact mais c'est juste un raccourci pour i = i + 1 et sous cette forme, il est plus facile de voir qu'elle implique une lecture et une écriture de 'i'. Sans verrouillage, elle n'est tout simplement pas sûre pour les fils.

2 autres arguments sont :

  • `++' n'est pas défini comme thread-safe (à l'abri des fils)
  • l'existence de Interlocked.Increment (ref int x)

La sécurité des fils est rare et coûteuse, elle sera clairement annoncée lorsqu'elle sera disponible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X