10
votes

Que se passe-t-il si deux threads lisent et écrivent le même morceau de mémoire

Je crois comprendre que si deux threads lisent du même morceau de mémoire, et aucun fil écrit à cette mémoire, l'opération est sûre. Cependant, je ne suis pas sûr de ce qui se passe si un thread lit et l'autre écrit. Ce qui se passerait? Le résultat est-il indéfini? Ou la lecture sera-t-elle juste piquante? Si une lecture obsolète n'est pas une préoccupation, il est-il correct d'avoir une liste de lecture non synchronisée à une variable? Ou est-il possible que les données soient corrompues, et ni la lecture ni l'écriture ne seraient correctes et qu'on devrait toujours synchroniser dans ce cas?

Je tiens à dire que j'ai appris que c'est l'affaire ultérieure, qu'une race Sur l'accès à la mémoire laisse l'état indéfini ... mais je ne me souviens pas d'où j'ai peut-être appris que et je suis difficile de trouver la réponse sur Google. Mon intuition est qu'une variable est utilisée dans des registres et que la concurrence vraie (comme dans le matériel) est impossible (ou est-ce), de sorte que le pire qui puisse arriver est des données obsolètes, c'est-à-dire les suivantes: P>

WriteThread: copy value from memory to register
WriteThread: update value in register
ReadThread:  copy value of memory to register
WriteThread: write new value to memory


0 commentaires

3 Réponses :


11
votes

Le résultat n'est indéfini. Les données corrompues sont entièrement possibles. Pour un exemple évident, considérez une valeur de 64 bits manipulée par un processeur 32 bits. Supposons que la valeur est un compteur simple et nous l'incruste lorsque les 32 bits inférieurs contiennent 0xFFFFFFFF. L'incrément produit 0x0000000000. Lorsque nous détectons cela, nous incremporons le mot supérieur. Si, toutefois, un autre thread lit la valeur entre le moment où le mot inférieur a été incrémenté et que le mot supérieur a été incrémenté, ils obtiennent une valeur avec un mot supérieur non incrémenté, mais le mot inférieur réglé à 0 - une valeur complètement différente de ce qu'il aurait été avant ou après la fin de l'incrément.


6 commentaires

Cela fait un sens parfait. Merci beaucoup. Pas à Nitpick, mais F ou le souci de compréhension, disons que je sais pour un fait que je suis sur un processeur 32 bits et que ma variable est de 32bits (ou moins). Si un seul thread est écrit et que d'autres threads sont en lecture, il y aura toujours le potentiel de corruption de données? En d'autres termes, est l'exemple que vous avez fourni le problème problème?


Si votre ordinateur offre des écrires atomiques 32 bits et qu'un seul thread écrit, la valeur observée par l'autre thread est soit l'ancienne valeur, soit la nouvelle valeur. C'est la base de l'algorithme de Dekker.


Si c'est exactement 32 bits, non. Si c'est moins, peut-être. En outre, cette garantie est que pour X86, PPC ou bras pourrait ne pas être aussi bienfaisante


Oui, il est toujours possible, au moins sur X86, si votre article 32 bits est mal aligné, de sorte qu'il traverse une ligne de cache. Je pense que le PPC et le bras empêchent que (pas en gardant l'accès atomique, mais en interdisant complètement ces accès mal alignés).


Unix fournit un type SIG_ATOMIC_T qui est garanti pour être une écriture atomique. Je suis sûr que Windows a quelque chose de similaire. Vous pouvez appeler Taillef (sig_atomic_t) pour connaître la taille sur une plate-forme particulière.


@Paulrubel: sig_atomic_t est réellement fourni par la norme C, il est donc disponible sur pratiquement toutes les plates-formes. Malheureusement, cela ne signifie pas beaucoup - il est garanti d'être atomique en présence d'interruptions asynchrones, mais pas nécessairement des threads. Les deux peuvent être les mêmes, mais encore une fois, ils ne peuvent pas.



11
votes

Habituellement, la mémoire est lue ou écrite dans des unités atomiques déterminées par l'architecture CPU (32 bits et 64 bits alignés sur des limites 32 bits et 64 bits sont courantes de nos jours).

Dans ce cas, quel qui arrive dépend de la quantité de données écrites.

Considérons le cas des cellules de lecture / écriture atomique 32 bits.

Si deux threads écrivent 32 bits dans une cellule aussi alignée, il est absolument bien défini ce qui se passe: l'une des deux valeurs écrites est conservée. Malheureusement pour vous (Eh bien, le programme), vous ne savez pas quelle valeur. Par une programmation extrêmement intelligente, vous pouvez en réalité utiliser cette atomicité de lectures et écrivies pour construire des algorithmes de synchronisation (par exemple, dekker algorithme ), mais il est généralement plus rapide d'utiliser des serrures définies sur l'architecture.

Si deux threads écrivent plus qu'une unité atomique (par exemple, ils écrivent une valeur de 128 bits), puis les morceaux de la taille de l'unité atomique des valeurs écrits seront stockés dans un puits absolument bien. manière définie, mais vous ne saura pas quelles pièces dont la valeur est écrite dans quel ordre. Donc, ce qui peut se retrouver dans le stockage est la valeur du premier thread, du deuxième thread ou de mélange des bits en tailles d'unité atomique à partir de deux filets.

Des idées similaires contiennent pour une lecture d'un fil et un fil écrit dans des unités atomiques et plus grandes.

Fondamentalement, vous ne voulez pas faire des lectures non synchronisées et écrit aux emplacements de mémoire, car vous ne connaîtrez pas le résultat, même s'il peut être très bien défini par l'architecture.


2 commentaires

N'oubliez pas que si vous utilisez des méthodes d'écriture «spéciales» sur les variables (c.-à-d. Interlockedxxx routines), sur les systèmes multicœurs, vous pouvez vous retrouver avec différentes valeurs pour les variables car la cache de la CPU n'a pas été synchronisé.


Je pensais que le X86 au moins proposé des caches cohérentes même sur des systèmes multicœurs. Je ne sais pas autant sur le bras ou le PPC, mais les caches incohérentes font de la programmation autant plus difficile. (Je pense que les gars de l'échelle de super-ordinateur abandonnent la cohérence du parallélisme pure si des cœurs distribués.)



0
votes

Comme je suis allongé dans IRA BAXTER 'S, la cache de la CPU joue également une partie sur les systèmes multicœurs. Considérez le code de test suivant:

Danger Robison!

Le code suivant augmente la priorité au temps réel pour obtenir des résultats quelque peu plus cohérents - en faisant ainsi nécessiter des privilèges d'administration, soyez prudent si vous exécutez le code sur Dual- ou des systèmes à base unique, puisque votre machine se verrouille pendant la durée de l'exécution du test. xxx

sur mon système Q6600 Intel Q6600 (que IIRC a deux ensembles de CORELS où chaque ensemble partager L2 cache - expliquerait les résultats quand même;)), je reçois les résultats suivants: xxx


10 commentaires

S'il s'agit d'un X86, ce que vous démontrez ici, c'est "Deux threads faisant des écrivies atomiques non synchronisées" non cache incohérences.


@Ira Baxter: n'est-ce pas exactement l'incohérences de cache, car les instructions n'incluent pas de préfixe de verrouillage et que les caches ne sont donc pas synchronisées?


@Snemarch: Je ne sais pas ce que vous entendez par "non synchronisé". X86 processeurs garantissent la cohérence du cache par le protocole MESI (Vérifiez les manuels de l'architecture X86); Si un processeur tente de toucher un emplacement détenu dans un cache par un autre, le protocole MESI garantit que la ligne de cache détenue par une CPU est transférée à la CPU demandeur afin que tous les processeurs voient une vue "cohérente" de chaque emplacement de mémoire. La serrure force la CPU émettrice de verrouillage pour maintenir la ligne de cache jusqu'à ce qu'elle soit faite avec, généralement une affaire de lecture et d'écriture.


@Ira Baxter: a fait un peu de lecture, et il semble que j'ai vraiment mal compris le préfixe de verrouillage - mon mauvais. Comment se passe le facteur ~ 10 différence entre les scénarios d'affinité expliqués, cependant?


@Snemarch: Quelle valeur que vous obtenez dépend du moment relativement des écritures. Changer l'affinité de processeur change cette relation de synchronisation. Si vos processeurs sont hyperthreadés, vous pouvez obtenir des effets vraiment étranges sur le moment, car vos programmes sont en concurrence pour le même processeur physique sous-jacent.


@Snemarch: Votre programme est toujours un moyen agréable de démontrer le problème posé par l'OP.


@Ira Baxter: Q6600 n'est pas hyperthreadisé - c'est un quadcore avec un ensemble complet d'unités d'exécution par noyau. IIRC La conception est fondamentalement "deux dualcores sur une matrice", où chaque Dualcore partage un bloc de cache L2 et une interconnexion rapide. Mais laissez-moi répéter, c'est quatre cœurs physiques, pas ht.


Et tandis que les horaires diffèrent d'un montant équitable par exécution (vous auriez besoin de faire un «système d'exploitation» nu - métal pour éviter cela), le facteur ~ 10x entre les groupes d'affinité est cohérent entre les courses.


@Snemarch: Comme vous l'avez noté, vous avez deux Dualcores. Ce n'est tout simplement pas parfaitement symétrique, vous ne devriez donc pas vous attendre à des résultats parfaitement symétriques de différentes tâches de processeur.


Assez tard pour faire cette remarque mais n'est pas le message: "Dague, Robinson?"