67 votes

Est-ce que l'appel de fonction est une barrière de mémoire efficace pour les plates-formes modernes?

Dans une base de code que j'ai examiné, j'ai trouvé la marche suivante.

void notify(struct actor_t act) {
    write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
    global.data = data;
    notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
    case 'M': use_data(global.data);break;
    ...
}

"Hold on", je l'ai dit à l'auteur, un haut membre de mon équipe, "il n'y a pas de barrière de mémoire ici! Vous n'avez pas de garantie que l' global.data seront supprimées du cache mémoire principale. Si Un thread et le fil B sera exécuté en deux processeurs différents - ce système peut échouer".

Le programmeur senior lui sourit et expliqua lentement, comme si l'explication de ses cinq ans garçon comment attacher ses lacets: "écoutez jeune garçon, nous l'avons vu ici beaucoup de fil bugs, en haut de test de charge, et dans la vraie clients", il s'arrêta à gratter ses longs poils de barbe, "mais nous n'avons jamais eu de bug avec cet idiome".

"mais, il est dit dans le livre..."

"assez!", il feutrée moi promptement,

"peut-être, théoriquement, il n'est pas garanti, dans la pratique, le fait que vous avez utilisé un appel de fonction est effectivement une barrière de mémoire, le compilateur ne va pas réorganiser l'instruction global.data = data, car il ne peut pas savoir si quelqu'un de l'utiliser dans l'appel de fonction, et l'architecture x86 veillera à ce que les autres Processeurs va voir ce morceau de données globales par le temps thread B lit la commande à partir de la pipe. Soyez rassurés, nous avons assez de problèmes du monde réel d'inquiétude, nous n'avons pas besoin d'investir des efforts supplémentaires dans de faux problèmes théoriques.

Soyez rassurés, mon garçon, dans le temps, vous comprendrez de séparer le vrai problème de la je-besoin-pour-obtenir-un-Doctorat non des problèmes."

Est-il correct? est-ce vraiment un problème dans la pratique (dire x86, x64 et ARM)?

C'est contre tout ce que j'ai appris, mais il a une longue barbe et un très joli look!

Des points supplémentaires si vous pouvez me montrer un bout de code prouver qu'il se trompait!

11voto

Mahmoud Al-Qudsi Points 14815

Les barrières de la mémoire ne sont pas seulement pour éviter d'instruction de la réorganisation. Même si les instructions ne sont pas réorganisées il peut encore causer des problèmes avec cohérence de cache. Comme pour le réagencement - cela dépend de votre compilateur et les paramètres. La CPI est particulièrement agressive avec réorganisation. MSVC w/ tout le programme d'optimisation peut être, trop.

Si vos données partagées variable est déclarée comme volatile, même si elle n'est pas dans la spec, la plupart des compilateurs va générer une variable de mémoire autour de lectures et d'écritures à partir de la variable et de l'empêcher de réorganisation. Ce n'est pas la bonne façon d'utiliser volatile, ni à quoi elle était destinée.

(Si j'avais des voix de gauche, je serais +1 à votre question pour la narration.)

9voto

janneb Points 17303

Dans la pratique, un appel de fonction est un compilateur de la barrière, ce qui signifie que le compilateur ne pourra pas bouger mondial de l'accès à la mémoire passé l'appel. Une mise en garde pour ce qui est des fonctions qui le compilateur sait quelque chose, par exemple, les builtins, inline fonctions (gardez à l'esprit PAPE!) etc.

Donc, un processeur, de la mémoire de la barrière (en plus d'un compilateur barrière) est en théorie nécessaire pour faire ce travail. Cependant, puisque vous êtes à l'appel de lecture et d'écriture qui sont syscalls que le changement de l'état global, je suis tout à fait sûr que le noyau émet les barrières de la mémoire quelque part dans la mise en œuvre de ceux-ci. Il n'y a aucune garantie si, donc, en théorie, vous avez besoin de les barrières.

3voto

Jens Gustedt Points 40410

Je ne sais pas si cela a été formulé comme ça dans les versions antérieures de la norme C11 a ce

Il y a un point de séquence d'après les évaluations de la fonction de désignation et les arguments, mais avant l'appel.

D'un point de séquence est une synchronisation forte et, en particulier, tous les effets secondaires de toutes les opérations avant que le point (comme la mise à jour global.data) doit avoir eu lieu avant l'appel. Dans le C11 modèle de thread de ces effets secondaires doivent alors tous être visibles pour les autres threads pour lesquelles vous pouvez prouver (et vous pouvez) que l'accès est livré après votre appel de fonction.

C avant C11 avait pas de modèle de thread, donc il n'est pas précisé ce que cela signifie pour un multi-threading de l'environnement et dépendra de votre plate-forme. AFAIR POSIX a des garanties similaires, et il serait vraiment surprenant que d'autres OS réagirais différemment.

Edit: En C11, séquencée avant le rapport décrit les intra-fil de la relation entre les opérations. Ces par thread relations sont collés ensemble pour une "passe avant" rapport qui porte sur tous les threads. Effets secondaires d'un objet qui sont exploités dans un thread qui peut être prouvée "dans les parages avant de" lors de un autre thread accède à ce même objet sont garantis pour être visible dans le second thread.

2voto

ams Points 10312

La règle de base est: le compilateur doit faire l'état global semble être exactement comme vous l'avez codé comme ça, mais si on peut prouver qu'une fonction donnée ne pas utiliser de variables globales, alors il peut mettre en œuvre l'algorithme de toute façon qu'il choisit.

Le résultat est que les compilateurs toujours traité fonctions dans une autre unité de compilation comme une barrière de mémoire parce qu'ils ne pouvaient pas voir à l'intérieur de ces fonctions. De plus en plus, les compilateurs modernes sont de plus en plus "tout le programme" ou "liaison" optimisation des stratégies de briser ces barrières et va causer mal écrit le code à l'échec, même si c'est très bien marché pendant des années.

Si la fonction en question est dans une bibliothèque partagée alors il ne sera pas en mesure de voir à l'intérieur, mais si la fonction est définie par la norme C, alors il n'a pas besoin d' -- il sait déjà ce que la fonction n'-de sorte que vous devez être prudent de ces également. Notez qu'un compilateur de ne pas reconnaître un noyau d'appel pour ce qu'il est, mais l'acte même de l'insertion de quelque chose que le compilateur ne peut pas reconnaître (assembleur en ligne, ou un appel de fonction à un assembleur de fichier) permet de créer une barrière de mémoire en lui-même.

Dans votre cas, notify soit une boîte noire, le compilateur ne peut pas voir à l'intérieur (une fonction de la bibliothèque) ou bien il va contenir une reconnaissables barrière de mémoire, de sorte que vous êtes plus susceptibles coffre-fort.

Dans la pratique, vous avez à écrire très mauvais code de tomber sur cette.

1voto

amadvance Points 41

En pratique, il a raison et une barrière de mémoire est impliquée dans ce cas particulier.

Mais le fait est que si sa présence est "discutable", le code est déjà trop complexe et peu clair.

Vraiment les gars, utilisez un mutex ou d'autres constructions appropriées. C'est le seul moyen sûr de gérer les threads et d'écrire du code maintenable.

Et peut-être verrez-vous d'autres erreurs, telles que le code est imprévisible si send () est appelé plus d'une fois.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X