Atomicité de la lecture 32 bits sur le processeur multicœur

(Remarque: j’ai ajouté des balises à cette question en fonction de l’endroit où, à mon avis, les gens seront susceptibles de pouvoir aider, alors veuillez ne pas crier :))

Dans mon projet VS 2017 64 bits, j’ai une valeur longue de 32 bits m_lClosed . Lorsque je souhaite mettre à jour cela, j’utilise l’une des fonctions de la famille Interlocked .

Considérez ce code en exécutant sur le thread # 1

 LONG lRet = InterlockedCompareExchange(&m_lClosed, 1, 0); // Set m_lClosed to 1 provided it's currently 0 

Considérons maintenant ce code en s’exécutant sur le thread n ° 2:

 if (m_lClosed) // Do something 

Je comprends que sur un seul processeur, cela ne posera pas de problème, car la mise à jour est atomique et la lecture l’est également (voir MSDN ). Par conséquent, la préemption de thread ne peut pas laisser la variable dans un état partiellement mis à jour. Mais sur un processeur multicœur, ces deux éléments de code pourraient s’exécuter en parallèle si chaque thread se trouve sur un processeur différent. Dans cet exemple, je ne pense pas que cela poserait un problème, mais il n’est toujours pas bon de tester quelque chose qui est en train d’être mis à jour.

Cette page Web me dit que l’atomicité sur plusieurs processeurs est obtenue via l’instruction d’assemblage LOCK , ce qui empêche les autres processeurs d’accéder à cette mémoire. Cela ressemble à ce dont j’ai besoin, mais le langage assembleur généré pour le test if ci-dessus est simplement

 cmp dword ptr [l],0 

… aucune instruction LOCK en vue.

Comment dans un événement comme celui-ci sums-nous supposés assurer l’atomicité de la lecture?

EDITER 24/4/18

Tout d’abord merci pour tout l’intérêt que cette question a généré. Je montre ci-dessous le code actuel; J’ai volontairement gardé la simplicité pour me concentrer sur l’atomicité de tout cela, mais de toute évidence, il aurait été mieux si j’avais tout montré de la première minute.

Deuxièmement, le projet dans lequel le code actuel réside est un projet VS2005; donc pas d’access aux atomes C ++ 11 . C’est pourquoi je n’ai pas ajouté la balise C ++ 11 à la question. J’utilise VS2017 avec un projet “scratch” pour ne pas avoir à construire l’énorme VS2005 chaque fois que je fais un changement en cours d’apprentissage. De plus, c’est un meilleur IDE.

Bien, le code actuel réside dans un serveur piloté par IOCP, et toute cette atomicité consiste à gérer un socket fermé:

 class CConnection { //... DWORD PostWSARecv() { if (!m_lClosed) return ::WSARecv(...); else return WSAESHUTDOWN; } bool SetClosed() { LONG lRet = InterlockedCompareExchange(&m_lClosed, 1, 0); // Set m_lClosed to 1 provided it's currently 0 // If the swap was carried out, the return value is the old value of m_lClosed, which should be 0. return lRet == 0; } SOCKET m_sock; LONG m_lClosed; }; 

L’appelant appellera SetClosed() ; si elle retourne true, elle appellera alors ::closesocket() etc. S’il vous plaît, ne vous ::closesocket() pas pourquoi c’est comme ça, c’est simplement 🙂

Considérez ce qui se passe si un thread ferme le socket pendant qu’un autre essaie de publier un WSARecv() . Vous pourriez penser que la WSARecv() échouera (le socket est fermé après tout!); Cependant, que se passe-t-il si une nouvelle connexion est établie avec le même descripteur de socket que celui que nous venons de fermer – nous WSARecv() le WSARecv() qui aboutira, mais cela serait fatal pour ma logique de programme car nous associons maintenant un tout autre connexion avec cet object CConnection. J’ai donc le test if (!m_lClosed) . Vous pourriez soutenir que je ne devrais pas gérer la même connexion dans plusieurs threads, mais ce n’est pas le but de cette question 🙂

C’est pourquoi je dois tester m_lClosed avant d’effectuer l’appel WSARecv() .

Maintenant, clairement, je ne fais que mettre m_lClosed à 1, donc une lecture / écriture déchirée n’est pas vraiment une préoccupation, mais c’est le principe qui me préoccupe . Que se passe-t-il si je règle m_lClosed sur 2147483647 et que je m_lClosed ensuite pour 2147483647? Dans ce cas, une lecture / écriture déchirée serait plus problématique.

Cela dépend vraiment de votre compilateur et du processeur sur lequel vous travaillez.

Les processeurs x86 liront de manière atomique les valeurs 32 bits sans le préfixe LOCK si l’adresse de la mémoire est correctement alignée. Cependant, vous aurez probablement besoin d’une sorte de barrière de mémoire pour contrôler l’exécution dans le désordre du CPU si la variable est utilisée comme verrou / décompte de certaines autres données associées. Les données qui ne sont pas alignées peuvent ne pas être lues de manière atomique, en particulier si la valeur chevauche une limite de page.

Si vous n’êtes pas un assembleur de codage manuel, vous devez également vous préoccuper des optimisations de réordonnancement des compilateurs .

Toute variable marquée comme volatile aura des contraintes d’ordre dans le compilateur (et éventuellement le code machine généré) lors de la compilation avec Visual C ++ :

Les propriétés insortingnsèques du compilateur _ReadBarrier, _WriteBarrier et _ReadWriteBarrier empêchent uniquement la réorganisation du compilateur. Avec Visual Studio 2003, les références volatiles à volatiles sont ordonnées. le compilateur ne réordonnera pas l’access aux variables volatiles. Avec Visual Studio 2005, le compilateur utilise également les sémantiques d’acquisition pour les opérations de lecture sur les variables volatiles et les sémantiques de publication pour les opérations d’écriture sur les variables volatiles (lorsqu’il est pris en charge par le CPU).

Améliorations spécifiques aux mots clés volatiles de Microsoft :

Lorsque l’option de compilateur / volatile: ms est utilisée (par défaut lorsque des architectures autres que ARM sont ciblées), le compilateur génère un code supplémentaire pour gérer l’ordre des références aux objects volatils en plus de l’ordre des références aux autres objects globaux. En particulier:

  • Une écriture dans un object volatile (également appelé écriture volatile) a la sémantique Release; c’est-à-dire qu’une référence à un object global ou statique se produit avant qu’une écriture dans un object volatile de la séquence d’instructions se produise avant cette écriture volatile dans le binary compilé.

  • Une lecture d’object volatile (également appelée lecture volatile) possède la sémantique Acquire; c’est-à-dire qu’une référence à un object global ou statique qui se produit après une lecture de mémoire volatile dans la séquence d’instructions se produira après cette lecture volatile dans le binary compilé.

Cela permet d’utiliser des objects volatiles pour les verrous de mémoire et les versions dans des applications multithread.


Pour les architectures autres que ARM, si aucune option du compilateur / volatile n’est spécifiée, le compilateur fonctionne comme si / volatile: ms avait été spécifié; Par conséquent, pour les architectures autres que ARM, nous vous recommandons vivement de spécifier / volatile: iso et d’utiliser des primitives de synchronisation explicites et les éléments insortingnsèques du compilateur lorsque vous utilisez de la mémoire partagée entre plusieurs threads.

Microsoft fournit des éléments insortingnsèques du compilateur pour la plupart des fonctions Interlocked * et celles-ci se comstackront sous la forme LOCK XADD ... au lieu d’un appel de fonction.

Jusqu’à “récemment”, C / C ++ ne prenait pas en charge les opérations atomiques ni les threads en général, mais cela changeait dans C11 / C ++ 11 où le support atomique était ajouté. L’utilisation de l’en-tête et de ses types / fonctions / classes déplace la responsabilité d’alignement et de réorganisation du compilateur afin que vous n’ayez pas à vous en préoccuper. Vous devez toujours faire un choix concernant les barrières de mémoire, ce qui détermine le code machine généré par le compilateur. Avec un ordre de mémoire assoupli, l’opération de load atomique finira très probablement par être une simple instruction MOV sur x86. Un ordre de mémoire plus ssortingct peut append une clôture et éventuellement le préfixe LOCK si le compilateur détermine que la plate-forme cible le requirejs.

En C ++ 11, un access non synchronisé à un object non atomique (tel que m_lClosed ) correspond à un comportement non défini.

La norme fournit toutes les installations dont vous avez besoin pour l’écrire correctement. vous n’avez pas besoin de fonctions non portables telles que InterlockedCompareExchange . Au lieu de cela, définissez simplement votre variable comme atomic :

 std::atomic m_lClosed{false}; // Writer thread... bool expected = false; m_lClosed.compare_exhange_strong(expected, true); // Reader... if (m_lClosed.load()) { /* ... */ } 

C’est plus que suffisant (cela force la cohérence séquentielle, ce qui peut être coûteux). Dans certains cas, il pourrait être possible de générer un code légèrement plus rapide en relâchant l’ordre de la mémoire sur les opérations atomiques, mais je ne m’inquiéterais pas pour cela.

Comme je l’ai signalé ici , cette question n’a jamais été sur la protection d’une section critique de code, mais uniquement sur la prévention des lectures / écritures déchirées. user3386109 a posté un commentaire ici que j’ai fini par utiliser, mais a refusé de l’afficher en tant que réponse ici . Je propose donc la solution que j’ai utilisée pour compléter cette question; peut-être que cela aidera quelqu’un à l’avenir.

Ce qui suit montre la configuration et les tests atomiques de m_lClosed :

 long m_lClosed = 0; 

Fil 1

 // Set flag to closed if (InterlockedCompareExchange(&m_lClosed, 1, 0) == 0) cout << "Closed OK!\n"; 

Fil 2

Ce code remplace if (!m_lClosed)

 if (InterlockedCompareExchange(&m_lClosed, 0, 0) == 0) cout << "Not closed!"; 

OK, alors il s’avère que ce n’est vraiment pas nécessaire; cette réponse explique en détail pourquoi nous n’avons pas besoin d’utiliser d’opérations verrouillées pour une lecture / écriture simple (nous le faisons pour une lecture-modification-écriture).