Je trouve souvent ces termes utilisés dans le contexte de la programmation concurrente. Sont-ils identiques ou différents ?
Réponses
Trop de publicités?Non, ce n'est pas la même chose. Ils ne sont pas un sous-ensemble l'un de l'autre. Ils ne sont également ni la condition nécessaire, ni la condition suffisante l'un pour l'autre.
La définition d'une course de données est assez claire, et par conséquent, sa découverte peut être automatisée. Une course aux données se produit lorsque 2 instructions de différents threads accèdent au même emplacement mémoire, qu'au moins un de ces accès est une écriture et qu'il n'y a pas de synchronisation obligatoire. tout un ordre particulier parmi ces accès.
Une condition de course est une erreur sémantique. Il s'agit d'un défaut qui se produit dans la synchronisation ou l'ordre des événements et qui entraîne un comportement erroné du programme. De nombreuses conditions de course peuvent être causées par des courses de données, mais ce n'est pas nécessaire.
Considérons l'exemple simple suivant où x est une variable partagée :
Thread 1 Thread 2
lock(l) lock(l)
x=1 x=2
unlock(l) unlock(l)
Dans cet exemple, les écritures en x des threads 1 et 2 sont protégées par des verrous, elles se produisent donc toujours dans un ordre imposé par l'ordre dans lequel les verrous sont acquis au moment de l'exécution. En d'autres termes, l'atomicité des écritures ne peut pas être rompue ; il existe toujours une relation "arrive avant" entre les deux écritures dans toute exécution. Nous ne pouvons simplement pas savoir a priori quelle écriture se produit avant l'autre.
Il n'y a pas d'ordre fixe entre les écritures, car les verrous ne peuvent pas le fournir. Si l'exactitude des programmes est compromise, par exemple lorsque l'écriture de x par le fil d'exécution 2 est suivie de l'écriture de x dans le fil d'exécution 1, nous disons qu'il y a une condition de course, bien que techniquement il n'y ait pas de course de données.
Il est beaucoup plus utile de détecter les conditions de course que les courses de données, mais cela est également très difficile à réaliser.
La construction de l'exemple inverse est également triviale. Ce site L'article du blog explique également très bien la différence, avec un exemple simple de transaction bancaire.
Selon Wikipedia, le terme "race condition" est utilisé depuis l'époque des premières portes logiques électroniques. Dans le contexte de Java, une condition de course peut concerner n'importe quelle ressource, comme un fichier, une connexion réseau, un thread d'un pool de threads, etc.
Il est préférable de réserver le terme "course aux données" à sa signification spécifique définie par la loi sur la protection des données. JLS .
Le cas le plus intéressant est une condition de course qui est très similaire à une course de données, mais qui n'en est pas une, comme dans cet exemple simple :
class Race {
static volatile int i;
static int uniqueInt() { return i++; }
}
Desde i
est volatile, il n'y a pas de course de données ; cependant, du point de vue de la correction du programme, il existe une condition de course due à la non-atomicité des deux opérations : lecture i
, écrivez i+1
. Plusieurs threads peuvent recevoir la même valeur de uniqueInt
.
TL;DR : La distinction entre course de données et condition de course dépend de la nature de la formulation du problème, et de l'endroit où l'on trace la frontière entre un comportement indéfini et un comportement bien défini mais indéterminé. La distinction actuelle est conventionnelle et reflète le mieux l'interface entre l'architecte du processeur et le langage de programmation.
1. Sémantique
La course aux données fait spécifiquement référence aux "accès à la mémoire" (ou actions, ou opérations) conflictuels non synchronisés au même emplacement mémoire. S'il n'y a pas de conflit dans les accès à la mémoire, mais qu'il y a toujours un comportement indéterminé causé par l'ordre des opérations, il s'agit d'une condition de course.
Notez que les "accès à la mémoire" ont ici une signification spécifique. Ils font référence aux actions "pures" de chargement ou de stockage de la mémoire, sans qu'aucune sémantique supplémentaire ne soit appliquée. Par exemple, un stockage en mémoire à partir d'un thread ne sait pas (nécessairement) combien de temps il faut pour que les données soient écrites dans la mémoire, et se propagent finalement à un autre thread. Autre exemple, un stockage en mémoire à un emplacement avant un autre stockage à un autre emplacement par le même thread ne garantit pas (nécessairement) que la première donnée écrite dans la mémoire précède la seconde. Par conséquent, l'ordre de ces accès purs à la mémoire n'est pas (nécessairement) en mesure d'être "raisonné" et tout peut arriver, à moins que cela ne soit bien défini.
Lorsque les "accès à la mémoire" sont bien définis en termes d'ordre par la synchronisation, une sémantique supplémentaire peut garantir que, même si le moment des accès à la mémoire est indéterminé, leur ordre peut être déterminé. "raisonné" à travers les synchronisations. Remarque : bien que l'ordre entre les accès à la mémoire puisse être raisonné, il n'est pas nécessairement déterminé, d'où la condition de course.
2. Pourquoi cette différence ?
Mais si l'ordre est toujours indéterminé en condition de course, pourquoi prendre la peine de le distinguer de la course aux données ? La raison est d'ordre pratique plutôt que théorique. C'est que la distinction existe bel et bien dans l'interface entre le langage de programmation et l'architecture du processeur.
Dans les architectures modernes, une instruction de chargement/stockage de mémoire est généralement mise en œuvre comme un accès "pur" à la mémoire, en raison de la nature du pipeline hors ordre, de la spéculation, du cache à plusieurs niveaux, de l'interconnexion entre processeur et mémoire centrale, en particulier des processeurs multicœurs, etc. De nombreux facteurs conduisent à un timing et un ordonnancement indéterminés. Il y a beaucoup de facteurs qui conduisent à un timing et un ordonnancement indéterminés. Faire respecter l'ordonnancement pour chaque instruction mémoire entraîne une pénalité énorme, en particulier dans une conception de processeur qui supporte le multi-cœur. La sémantique de l'ordonnancement est donc fournie par des instructions supplémentaires, comme diverses barrières (ou fences).
La course aux données est la situation d'exécution d'une instruction du processeur sans barrières supplémentaires pour aider à raisonner l'ordre d'accès conflictuels à la mémoire. Le résultat n'est pas seulement indéterminé, mais peut aussi être très bizarre, par exemple, deux écritures au même emplacement de mot par des threads différents peuvent résulter en l'écriture de la moitié du mot par chacun d'eux, ou peuvent opérer uniquement sur leurs valeurs mises en cache localement. -- Ce sont des comportements non définis, du point de vue du programmeur. Mais ils sont (généralement) bien définis du point de vue de l'architecte du processeur.
Les programmeurs doivent avoir un moyen de raison l'exécution de leur code. La course aux données est quelque chose qu'ils ne peuvent pas comprendre et qu'ils doivent donc toujours éviter (normalement). C'est pourquoi les spécifications des langages qui sont suffisamment bas niveau définissent généralement la course aux données comme un comportement non défini, différent du comportement bien défini des conditions de course en mémoire.
3. Modèles de mémoire linguistique
Des processeurs différents peuvent avoir un comportement d'accès à la mémoire différent, c'est-à-dire un modèle de mémoire de processeur différent. Il est maladroit pour les programmeurs d'étudier le modèle de mémoire de chaque processeur moderne et de développer ensuite des programmes qui peuvent en bénéficier. Il est souhaitable que le langage puisse définir un modèle de mémoire afin que les programmes de ce langage se comportent toujours comme prévu selon le modèle de mémoire défini. C'est pourquoi Java et C++ ont leurs modèles de mémoire définis. Il incombe aux développeurs du compilateur et du moteur d'exécution de s'assurer que les modèles de mémoire du langage sont appliqués sur les différentes architectures de processeur.
Cela dit, si un langage ne veut pas exposer le comportement de bas niveau du processeur (et est prêt à sacrifier certains avantages en termes de performances des architectures modernes), il peut choisir de définir un modèle de mémoire qui cache complètement les détails des accès mémoire "purs", mais applique une sémantique d'ordonnancement pour toutes ses opérations mémoire. Les développeurs du compilateur et du moteur d'exécution peuvent alors choisir de traiter chaque variable mémoire comme volatile dans toutes les architectures de processeur. Pour ces langages (qui supportent la mémoire partagée entre les threads), il n'y a pas de courses de données, mais il peut toujours y avoir des conditions de course, même avec un langage de cohérence séquentielle complète.
D'autre part, le modèle de mémoire du processeur peut être plus strict (ou moins relaxé, ou à un niveau plus élevé), par exemple en implémentant la cohérence séquentielle comme le faisaient les premiers processeurs. Dans ce cas, toutes les opérations de mémoire sont ordonnées, et aucune course aux données n'existe pour les langages s'exécutant dans le processeur.
4. Conclusion
Pour en revenir à la question initiale, je pense qu'il est bon de définir la course aux données comme un cas particulier de condition de course, et la condition de course à un niveau peut devenir une course aux données à un niveau supérieur. Cela dépend de la nature de la formulation du problème et de l'endroit où l'on trace la frontière entre un comportement non défini et un comportement bien défini mais indéterminé. Le fait que la convention actuelle définisse la limite à l'interface langage-processeur ne signifie pas nécessairement que c'est toujours et obligatoirement le cas ; mais la convention actuelle reflète probablement le mieux l'interface (et la sagesse) de l'état de l'art entre l'architecte du processeur et le langage de programmation.
Non, ils sont différents & aucun d'entre eux n'est un sous-ensemble de l'un ou de l'autre ou vice-versa.
Le terme "condition de course" est souvent confondu avec le terme connexe "données". qui se produit lorsque la synchronisation n'est pas utilisée pour coordonner tous les accès à un champ non final partagé. accès à un champ partagé non final. Vous risquez une course aux données lorsqu'un un thread écrit une variable qui pourrait ensuite être lue par un autre thread ou lit une variable qui pourrait avoir été écrite en dernier par un autre thread si les deux threads n'utilisent pas la synchronisation. n'a pas de sémantique définie utile dans le cadre du modèle de mémoire Java. Toutes les conditions de course ne sont pas toutes des courses de données, et toutes les courses de données ne sont pas des conditions de course, mais elles peuvent toutes deux faire échouer des programmes concurrents de manière imprévisible. imprévisibles.
Tiré de l'excellent livre - Java Concurrency in Practice par Joshua Bloch & Co.