Question

Auparavant, j'avais écrit du code multithread très simple et j'ai toujours été conscient qu'à tout moment, il pouvait y avoir un changement de contexte en plein milieu de ce que je fais, j'ai donc toujours gardé l'accès aux variables partagées via une classe CCriticalSection qui entre dans la section critique lors de la construction et la quitte lors de la destruction.Je sais que c'est assez agressif et j'entre et sors des sections critiques assez fréquemment et parfois de manière flagrante (par ex.au début d'une fonction alors que je pouvais placer la CCriticalSection dans un bloc de code plus serré) mais mon code ne plante pas et il s'exécute assez vite.

Au travail, mon code multithread doit être plus strict, uniquement verrouillé/synchronisé au niveau le plus bas nécessaire.

Au travail, j'essayais de déboguer du code multithread et je suis tombé sur ceci :

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

Maintenant, m_bSomeVariable est un Win32 BOOL (non volatile), qui, pour autant que je sache, est défini comme un int, et sur x86, la lecture et l'écriture de ces valeurs sont une seule instruction, et puisque les changements de contexte se produisent sur une limite d'instruction, il n'est pas nécessaire de synchroniser cette opération avec une section critique.

J'ai fait quelques recherches supplémentaires en ligne pour voir si cette opération ne nécessitait pas de synchronisation, et j'ai proposé deux scénarios :

  1. Le processeur implémente une exécution dans le désordre ou le deuxième thread s'exécute sur un cœur différent et la valeur mise à jour n'est pas écrite dans la RAM pour que l'autre cœur puisse la voir ;et
  2. L'int n'est pas aligné sur 4 octets.

Je crois que le numéro 1 peut être résolu en utilisant le mot-clé "volatile".Dans VS2005 et versions ultérieures, le compilateur C++ entoure l'accès à cette variable à l'aide de barrières de mémoire, garantissant que la variable est toujours complètement écrite/lue dans la mémoire principale du système avant de l'utiliser.

Numéro 2, je ne peux pas vérifier, je ne sais pas pourquoi l'alignement des octets ferait une différence.Je ne connais pas le jeu d'instructions x86, mais est-ce que mov faut-il recevoir une adresse alignée sur 4 octets ?Sinon, devez-vous utiliser une combinaison d’instructions ?Cela présenterait le problème.

Donc...

QUESTION 1: L'utilisation du mot-clé "volatile" (implicite utilisant des barrières de mémoire et indiquant au compilateur de ne pas optimiser ce code) dispense-t-elle un programmeur de la nécessité de synchroniser une variable 4 octets/8 octets sur x86/x64 entre les opérations de lecture/écriture ?

QUESTION 2: Existe-t-il une exigence explicite selon laquelle la variable doit être alignée sur 4 octets/8 octets ?

J'ai approfondi notre code et les variables définies dans la classe :

class CExample
{

private:

    CRITICAL_SECTION m_Crit1; // Protects variable a
    CRITICAL_SECTION m_Crit2; // Protects variable b
    CRITICAL_SECTION m_Crit3; // Protects variable c
    CRITICAL_SECTION m_Crit4; // Protects variable d

    // ...

};

Maintenant, cela me semble excessif.Je pensais que les sections critiques synchronisaient les threads entre un processus, donc si vous en avez un, vous pouvez y entrer et aucun autre thread de ce processus ne peut s'exécuter.Il n'est pas nécessaire d'avoir une section critique pour chaque variable que vous souhaitez protéger, si vous êtes dans une section critique alors rien d'autre ne peut vous interrompre.

Je pense que la seule chose qui peut changer les variables de l'extérieur d'une section critique est si le processus partage une page mémoire avec un autre processus (pouvez-vous faire cela ?) et que l'autre processus commence à modifier les valeurs.Les mutex seraient également utiles ici, les mutex nommés sont partagés entre les processus, ou uniquement les processus du même nom ?

QUESTION 3: Mon analyse des sections critiques est-elle correcte et ce code doit-il être réécrit pour utiliser des mutex ?J'ai jeté un œil à d'autres objets de synchronisation (sémaphores et spinlocks), sont-ils mieux adaptés ici ?

QUESTION 4 : Où les sections/mutex/sémaphores/spinlocks critiques sont-ils les mieux adaptés ?Autrement dit, à quel problème de synchronisation doivent-ils être appliqués.Y a-t-il une énorme pénalité en termes de performances pour choisir l’un plutôt que l’autre ?

Et pendant que nous y sommes, j'ai lu que les spinlocks ne devraient pas être utilisés dans un environnement multithread monocœur, mais uniquement dans un environnement multithread multicœur.Donc, QUESTION 5 : Est-ce faux, ou sinon, pourquoi est-ce juste ?

Merci d'avance pour vos réponses :)

Était-ce utile?

La solution

Q1: En utilisant le mot-clé "volatile"

  

Dans VS2005 et plus tard, le compilateur C ++ entoure l'accès à cette variable en utilisant des barrières de mémoire, assurant que la variable est toujours complètement écrit / lu à la mémoire principale du système avant de l'utiliser.

Exactement. Si vous n'êtes pas en train de créer un code portable, Visual Studio implémente exactement de cette façon. Si vous voulez être portable, vos options sont actuellement « limitées ». Jusqu'à C ++ 0x il n'y a aucun moyen portable comment spécifier des opérations atomiques avec la commande de lecture / écriture garantie et vous avez besoin de mettre en œuvre des solutions par plate-forme. Cela dit, coup de pouce a déjà fait le sale boulot pour vous, et vous pouvez utiliser son atomique primitives .

Q2: besoins variable à 4 octets / 8 octets aligné

?

Si vous ne les gardez alignés, vous êtes en sécurité. Si vous ne le faites pas, les règles sont compliquées (lignes de cache, ...), donc le moyen le plus sûr est de les garder alignés, comme cela est facile à réaliser.

Q3: Si ce code est réécrite pour utiliser mutex

Section critique est un mutex léger. À moins que vous devez synchroniser entre les processus, utiliser des sections critiques.

Q4: Où sont les sections critiques / mutex / sémaphores / spinlocks mieux adapté

peut même < a href = "http://msdn.microsoft.com/en-us/library/ms683476%28v=VS.85%29.aspx" rel = "nofollow noreferrer"> ne attend sPIN pour vous.

Q5: Spinlocks ne doivent pas être utilisés dans un seul noyau

Verrouillage Spin utilise le fait que si le CPU d'attente tourne, une autre CPU peut libérer le verrou. Cela ne peut pas se produire avec une CPU uniquement, il est donc seulement une perte de temps. Sur les verrous de spin multi-CPU peut être une bonne idée, mais cela dépend de la fréquence à laquelle l'attente de rotation sera couronnée de succès. L'idée est en attente d'un peu de temps est beaucoup plus rapide que le contexte faisant passer là-bas et à nouveau, donc si l'attendre qu'elle risque d'être bref, il est préférable d'attendre.

Autres conseils

1) Non volatile dit simplement de recharger la valeur de la mémoire chaque fois qu'il est TOUJOURS possible qu'elle soit à moitié mise à jour.

Modifier:2) Windows fournit certaines fonctions atomiques.Recherchez le Fonctions "verrouillées".

Les commentaires m'ont amené à lire un peu plus.Si vous lisez le Guide de programmation du système Intel Vous pouvez voir que les lectures et écritures alignées SONT atomiques.

8.1.1 Opérations atomiques garanties Le processeur Intel486 (et les nouveaux processeurs depuis) ​​garantit que les opérations de base de base suivantes seront toujours effectuées atomiquement:
• Lire ou écrire un octet
• Lire ou écrire un mot aligné sur une limite de 16 bits
• Lire ou écrire un mot double aligné sur une limite de 32 bits
Le processeur Pentium (et les nouveaux processeurs depuis) ​​garantit que les opérations de mémoire supplémentaires suivantes seront toujours effectuées atomiquement:
• Lecture ou écriture d'un quadword aligné sur une limite de 64 bits
• Accès 16 bits aux emplacements mémoire non mis en cache qui s'inscrivent dans un bus de données 32 bits.
Les processeurs familiaux P6 (et les nouveaux processeurs depuis) ​​garantissent que l'opération de mémoire supplémentaire suivante sera toujours effectuée atomiquement:
• Accès non aligné 16, 32 et 64 bits
Accès à la mémoire pouvant être mise en cache qui sont répartis entre les largeurs de bus, les lignes de cache et Les limites de page ne sont pas garanties atomiques par l’Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, famille P6, Pentium et Processeurs Intel486.Les processeurs Intel Core 2 Duo, Intel Atom, Intel Core Duo, Pentium M, Les processeurs des familles Pentium 4, Intel Xeon et P6 fournissent des signaux de contrôle de bus qui permettre aux sous-systèmes de mémoire externes de rendre les accès fractionnés atomiques ;toutefois Les accès aux données non alignés auront un impact sérieux sur les performances du processeur et doivent être évités.Une instruction x87 ou une instruction SSE qui accède à des données plus grandes qu’un quadword peut être implémenté à l’aide de plusieurs accès mémoire.Si une telle instruction stocke à la mémoire, certains des accès peuvent se terminer (écriture en mémoire) tandis qu’un autre provoque une défaillance de l’opération pour des raisons architecturales (p. ex.en raison d’une entrée de table de page qui porte la mention « non présent »).Dans ce cas, les effets des accès terminés peut être visible par le logiciel même si l’instruction globale a causé une erreur.Si TLB l’invalidation a été retardée (voir Section 4.10.3.4), de telles erreurs de page peuvent se produire même si tous les accès sont sur la même page.

Donc, fondamentalement, oui, si vous effectuez une lecture/écriture de 8 bits à partir de n'importe quelle adresse, une lecture/écriture de 16 bits à partir d'une adresse alignée sur 16 bits, etc., vous obtenez des opérations atomiques.Il est également intéressant de noter que vous pouvez effectuer des lectures/écritures de mémoire non alignées dans une ligne de cache sur une machine moderne.Les règles semblent cependant assez complexes, donc je ne m'y fierais pas si j'étais vous.Bravo aux commentateurs, c'est une bonne expérience d'apprentissage pour moi celui-là :)

3) Une section critique tentera de verrouiller son verrou plusieurs fois, puis verrouillera un mutex.Spin Locking peut consommer de la puissance du processeur sans rien faire et un mutex peut prendre un certain temps pour faire son travail.Les CriticalSections sont un bon choix si vous ne pouvez pas utiliser les fonctions verrouillées.

4) Il existe des pénalités de performance pour le choix de l'un plutôt que de l'autre.C'est une tâche assez importante que de passer en revue les avantages de tout ici.L'aide MSDN contient de nombreuses informations utiles sur chacun d'entre eux.Je suggère de les lire.

5) Vous pouvez utiliser un verrou tournant dans un environnement à thread unique, mais ce n'est généralement pas nécessaire, car la gestion des threads signifie que vous ne pouvez pas avoir 2 processeurs accédant simultanément aux mêmes données.Ce n'est tout simplement pas possible.

1:Volatil en soi est pratiquement inutile pour le multithreading.Il garantit que la lecture/écriture sera exécutée, plutôt que de stocker la valeur dans un registre, et garantit que la lecture/écriture ne sera pas réorganisée. par rapport aux autres volatile lit/écrit.Mais il peut toujours être réorganisé par rapport aux éléments non volatils, qui représentent essentiellement 99,9 % de votre code.Microsoft a redéfini volatile pour envelopper également tous les accès dans des barrières de mémoire, mais cela n'est pas garanti que ce soit le cas en général.Il se brisera silencieusement sur n'importe quel compilateur qui définit volatile comme le fait la norme.(Le code sera compilé et exécuté, il ne sera tout simplement plus thread-safe)

En dehors de cela, les lectures/écritures sur des objets de taille entière sont atomiques sur x86 tant que l'objet est bien aligné.(Vous n'avez aucune garantie de quand l'écriture aura lieu cependant.Le compilateur et le processeur peuvent le réorganiser, donc c'est atomique, mais pas thread-safe)

2 :Oui, l'objet doit être aligné pour que la lecture/écriture soit atomique.

3 :Pas vraiment.Un seul thread peut exécuter du code à l'intérieur d'une section critique donnée à la fois.D'autres threads peuvent toujours exécuter un autre code.Vous pouvez donc avoir quatre variables protégées chacune par une section critique différente.S'ils partageaient tous la même section critique, je serais incapable de manipuler l'objet 1 pendant que vous manipulez l'objet 2, ce qui est inefficace et contraint le parallélisme plus que nécessaire.S'ils sont protégés par des sections critiques différentes, nous ne pouvons tout simplement pas manipuler le même objet simultanément.

4 :Les spinlocks sont rarement une bonne idée.Ils sont utiles si vous vous attendez à ce qu'un thread n'attende que très peu de temps avant de pouvoir acquérir le verrou, et vous avez absolument besoin d'une latence minimale.Cela évite le changement de contexte du système d’exploitation qui est une opération relativement lente.Au lieu de cela, le thread reste simplement dans une boucle interrogeant constamment une variable.Donc une utilisation CPU plus élevée (le core n'est pas libéré pour exécuter un autre thread en attendant le spinlock), mais le thread pourra continuer dès que lorsque le verrou est libéré.

Quant aux autres, les caractéristiques de performances sont à peu près les mêmes :utilisez simplement celle qui a la sémantique la mieux adaptée à vos besoins.Les sections généralement critiques sont les plus pratiques pour protéger les variables partagées, et les mutex peuvent être facilement utilisés pour définir un « drapeau » permettant à d'autres threads de continuer.

Quant à ne pas utiliser de spinlocks dans un environnement monocœur, rappelez-vous que le spinlock ne cède pas réellement.Le thread A en attente d'un spinlock n'est pas réellement mis en attente, ce qui permet au système d'exploitation de planifier l'exécution du thread B.Mais puisque A attend ce verrou tournant, un autre thread devra libérer ce verrou.Si vous n'avez qu'un seul cœur, cet autre thread ne pourra s'exécuter que lorsque A sera éteint.Avec un système d'exploitation sain d'esprit, cela se produira de toute façon tôt ou tard dans le cadre du changement de contexte régulier.Mais puisque nous savons que A ne pourra pas obtenir le verrou tant que B n'aura pas eu le temps de l'exécuter et libérer le verrou, nous serions mieux si A cédait immédiatement, était mis dans une file d'attente par le système d'exploitation et redémarrait lorsque B a libéré le verrou.Et c'est tout ce que autre les types de verrous le font.Un spinlock sera toujours travail dans un environnement monocœur (en supposant un système d'exploitation avec multitâche préemptif), ce sera tout simplement très très inefficace.

Ne pas utiliser volatile. Il n'a pratiquement rien à voir avec la sécurité des threads. Voir ici pour le bas vers le bas.

L'affectation à BOOL n'a pas besoin de primitives de synchronisation. Ça va bien travailler sans effort particulier de votre part.

Si vous voulez définir la variable et assurez-vous qu'un autre thread voit la nouvelle valeur, vous devez établir une sorte de communication entre les deux fils. verrouillage juste immédiatement avant d'attribuer rien ne permette d'obtenir parce que l'autre thread aurait pu venir allé avant l'acquisition de la serrure.

Un dernier mot d'avertissement: le filetage est extrêmement difficile d'obtenir le droit. Les programmeurs les plus expérimentés ont tendance à être le moins à l'aise avec l'aide de fils, qui devrait fixer la sonnette d'alarme pour tous ceux qui manque d'expérience avec leur utilisation. Je vous suggère fortement d'utiliser des primitives de plus haut niveau pour mettre en œuvre dans votre application concurrency. Passe structures de données immuables via des files d'attente synchronisé est une approche qui réduit sensiblement le danger.

Volatile ne signifie pas les barrières de mémoire.

Cela signifie seulement que ce sera une partie de l'état perçu du modèle de mémoire. L'implication est que le compilateur ne peut pas optimiser loin la variable, et ne peut pas effectuer des opérations sur la variable que dans les registres CPU (il fait charger et stocker à la mémoire).

Comme il n'y a pas de barrières de mémoire implicite, le compilateur peut réorganiser les instructions à volonté. La seule garantie est que l'ordre dans lequel les différentes variables volatiles sont en lecture / écriture sera le même que dans le code:

void test() 
{
    volatile int a;
    volatile int b;
    int c;

    c = 1;
    a = 5;
    b = 3;
}

Avec le code ci-dessus (en supposant que c n'est pas optimisé loin) la mise à jour c peut se produire avant ou après les mises à jour de a et b, fournissant 3 résultats possibles. Les mises à jour de a et b sont garanties à effectuer dans l'ordre. c peut être optimisée à une distance facilement par un compilateur. Avec suffisamment d'informations, le compilateur peut même optimiser loin a et b (si l'on peut prouver qu'il n'y a pas d'autres threads lisent les variables et qu'ils ne sont pas liés à un ensemble de matériel (dans ce cas, ils peuvent en effet être supprimés). Notez que la norme ne nécessite pas un comportement spécifique, mais plutôt un état percevable à la règle de as-if.

Questions 3: CRITICAL_SECTIONs et travail mutex, à peu près, de la même façon. Un mutex Win32 est un objet noyau, il peut donc être partagée entre les processus, et attendit avec WaitForMultipleObjects, que vous ne pouvez pas faire avec un CRITICAL_SECTION. D'autre part, un CRITICAL_SECTION est donc plus rapide et plus léger. Mais la logique du code ne doit pas être affecté par que vous utilisez.

Vous avez également fait remarquer que « il n'y a pas besoin d'une section critique pour chaque variable que vous voulez protéger, si vous êtes dans une section critique alors rien ne peut vous interrompre autre. » Cela est vrai, mais le compromis est que les accès à l'une des variables aurait besoin de vous pour maintenir ce verrou. Si les variables peuvent être mises à jour de manière significative, vous perdez indépendamment l'occasion pour paralléliser ces opérations. (Puisque ce sont des membres du même objet, bien que, je pense dur avant de conclure qu'ils peuvent vraiment être accessibles indépendamment les uns des autres.)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top