web-dev-qa-db-fra.com

Est-il prudent de lire une variable entière modifiée simultanément sans verrouillage?

Supposons que j'ai une variable entière dans une classe et que cette variable puisse être modifiée simultanément par d'autres threads. Les écritures sont protégées par un mutex. Dois-je protéger les lectures aussi? J'ai entendu dire qu'il y a des architectures matérielles sur lesquelles, si un thread modifie une variable et qu'un autre le lit, le résultat de la lecture sera louche; dans ce cas, je dois protéger les lectures. Je n'ai jamais vu de telles architectures.

Cette question suppose qu'une seule transaction consiste à mettre à jour une seule variable entière, je ne m'inquiète donc pas de l'état de toute autre variable pouvant également être impliquée dans une transaction.

45
Hongli

lecture atomique
Comme dit précédemment, cela dépend de la plate-forme. Sur x86, la valeur doit être alignée sur une limite de 4 octets. En règle générale, pour la plupart des plates-formes, la lecture doit s’exécuter dans une seule instruction de la CPU.

Mise en cache de l'optimiseur
L'optimiseur ne sait pas que vous lisez une valeur modifiée par un autre thread. Déclarer la valeur volatile aide à cela: l'optimiseur émettra une lecture/écriture en mémoire pour chaque accès, au lieu d'essayer de conserver la valeur en cache dans un registre.

Cache du processeur
Néanmoins, vous pouvez lire une valeur périmée, car dans les architectures modernes, vous avez plusieurs cœurs avec un cache individuel qui n'est pas automatiquement synchronisé. Vous avez besoin d'une barrière de mémoire en lecture, généralement une instruction spécifique à la plate-forme. 

Sur Wintel, les fonctions de synchronisation des tâches ajoutent automatiquement une barrière de mémoire complète. Vous pouvez également utiliser les fonctions InterlockedXxxx .

MSDN: Problèmes de mémoire et de synchronisation , MemoryBarrier Macro

[edit] s'il vous plaît voir aussi les commentaires de drhirsch.

33
peterchen

Vous posez une question sur la lecture d'une variable et vous parlez ensuite de la mise à jour d'une variable, ce qui implique une opération de lecture-modification-écriture.

En supposant que vous entendiez vraiment l'ancien, la lecture est sûre s'il s'agit d'une opération atomique . Pour presque toutes les architectures, cela est vrai pour les entiers.

Il y a quelques exceptions (et rares):

  • La lecture est mal alignée, par exemple en accédant à un entier de 4 octets à une adresse impaire. Généralement, vous devez forcer le compilateur avec des attributs spéciaux à effectuer un désalignement.
  • La taille d'un int est plus grande que la taille naturelle des instructions, par exemple en utilisant des ints 16 bits sur une architecture 8 bits.
  • Certaines architectures ont une largeur de bus limitée artificiellement. Je ne connais que des modèles très anciens et obsolètes, comme un 386sx ou un 68008.
14
Gunther Piez

Je recommanderais de ne pas compter sur un compilateur ou une architecture dans ce cas.
Chaque fois que vous avez un mélange de lecteurs et d’écrivains (par opposition à seulement des lecteurs ou des écrivains), vous feriez mieux de les synchroniser tous. Imaginez que votre code exécute le cœur artificiel de quelqu'un, vous ne voulez pas vraiment qu'il lise de mauvaises valeurs, et vous ne voulez sûrement pas d'une centrale électrique dans votre ville, allez-y «boooom» parce que quelqu'un a décidé de ne pas utiliser ce mutex. Epargnez-vous une nuit de sommeil à long terme, synchronisez-les.
Si vous ne lisez qu'un seul fil - vous êtes prêt à utiliser ce mutex, mais si vous prévoyez plusieurs lecteurs et plusieurs rédacteurs, vous aurez besoin d'un morceau de code sophistiqué pour le synchroniser. . Une mise en œuvre intéressante du verrou en lecture/écriture qui serait également «juste» reste à voir pour moi.

8
Dmitry

Imaginez que vous lisiez la variable dans un seul thread, que ce thread soit interrompu pendant la lecture et que la variable soit modifiée par un thread en écriture. Maintenant, quelle est la valeur de l'entier lu après la reprise du fil de lecture?

À moins que la lecture d'une variable soit une opération atomique, dans ce cas, ne prend qu'une seule instruction (Assembly), vous ne pouvez pas garantir que la situation ci-dessus ne se produira pas. (La variable peut être écrite en mémoire et l'extraction de la valeur prend plus d'une instruction.)

Le consensus est que vous devez encapsuler/verrouiller toutes les écritures individuellement, alors que les lectures peuvent être exécutées simultanément avec (uniquement) les autres lectures.

5
NomeN

Supposons que j'ai une variable entière dans une classe et que cette variable puisse être modifiée simultanément par d'autres threads. Les écritures sont protégées par un mutex. Dois-je protéger les lectures aussi? J'ai entendu dire qu'il y a des architectures matérielles sur lesquelles, si un thread modifie une variable et qu'un autre le lit, le résultat de la lecture sera louche; dans ce cas, je dois protéger les lectures. Je n'ai jamais vu de telles architectures.

Dans le cas général, cela représente potentiellement every architecture. Toutes les architectures présentent des cas où lire simultanément avec une écriture entraînerait des erreurs. Cependant, presque toutes les architectures ont également des exceptions à cette règle.

Il est courant que les variables de la taille d'un mot soient lues et écrites de manière atomique. Par conséquent, la synchronisation n'est pas nécessaire lors de la lecture ou écriture. La valeur appropriée sera écrite atomiquement en tant qu'opération unique et les threads liront également la valeur current comme une seule opération atomique, même si un autre thread est en cours d'écriture. Donc pour les entiers, vous êtes en sécurité sur la plupart architectures. Certains vont étendre cette garantie à quelques autres tailles également, mais cela dépend évidemment du matériel.

Pour les variables de taille non Word, la lecture et l'écriture seront généralement non atomiques et devront être synchronisées par d'autres moyens.

3
jalf

Si vous n'utilisez pas la valeur précédente de cette variable lors d'une nouvelle écriture, alors:

Vous pouvez lire et écrire une variable entière sans utiliser mutex. C'est parce que integer est un type de base dans une architecture 32 bits et que chaque modification/lecture de valeur s'effectue avec une seule opération.

Mais si vous faites quelque chose comme un incrément:

myvar++;

Ensuite, vous devez utiliser mutex, car cette construction est étendue à myvar = myvar + 1 et entre read myvar et increment myvar, myvar peut être modifié. Dans ce cas, vous aurez une mauvaise valeur.

2
Vitaly Dyatlov

Bien qu'il soit probablement sûr de lire les fichiers sur des systèmes 32 bits sans synchronisation. Je ne risquerais pas ça. Bien que plusieurs lectures simultanées ne posent pas de problème, je n'aime pas que les écritures aient lieu en même temps que les lectures. 

Je recommanderais également de placer les lectures dans la section critique, puis de soumettre votre application à des tests de contrainte sur plusieurs cœurs pour voir si cela provoque trop de conflits. La recherche de bugs de concurrence est un cauchemar que je préfère éviter. Que se passe-t-il si, à l'avenir, quelqu'un décide de changer l'int en un long long ou un double, afin de pouvoir conserver un plus grand nombre?

Si vous avez une bibliothèque de threads Nice comme boost.thread ou zthread, alors vous devriez avoir des verrous en lecture/écriture. Celles-ci seraient idéales pour votre situation car elles permettent plusieurs lectures tout en protégeant les écritures.

2
iain

Cela peut se produire sur les systèmes 8 bits qui utilisent des entiers 16 bits.

Si vous souhaitez éviter le verrouillage, vous pouvez, dans certaines circonstances, lire plusieurs fois, jusqu'à obtenir deux valeurs égales consécutives. Par exemple, j'ai utilisé cette approche pour lire l'horloge 64 bits sur une cible intégrée à 32 bits, où le tic-tac d'horloge était implémenté en tant que routine d'interruption. Dans ce cas, trois lectures suffisent, car l'horloge ne peut cocher qu'une seule fois pendant la courte durée d'exécution de la routine de lecture.

1
starblue

En général, chaque instruction d'ordinateur passe par plusieurs étapes matérielles lors de l'exécution. Comme la plupart des processeurs actuels sont multi-cœurs ou hyper-threadés, cela signifie que la lecture d'une variable peut commencer à la déplacer par le biais du pipeline d'instructions, mais cela n'empêche pas un autre coeur ou hyper-thread d'un processeur d'exécuter simultanément une instruction de stockage sur le même adresse. Les deux instructions, lue et stockée, exécutées simultanément, peuvent "se croiser", ce qui signifie que la lecture recevra l'ancienne valeur juste avant que la nouvelle valeur ne soit stockée.
Pour résumer: vous avez besoin du mutex à la fois en lecture et en écriture.

0
harrymc

La lecture et l’écriture sur des variables avec concurrence doivent être protégées par une section critique (non mutex). Sauf si vous voulez perdre toute votre journée de débogage.

Les sections critiques sont spécifiques à la plate-forme, je crois. Sous Win32, la section critique est très efficace: en l’absence de verrouillage, l’entrée de la section critique est presque libre et n’affecte pas les performances globales. Lorsque le verrouillage s’effectue, il est toujours plus efficace que mutex, car il met en œuvre une série de vérifications avant de suspendre le thread.

0
SadSido