9
votes

Un compilateur C / C ++ peut-il légalement mettre en cache une variable dans un registre à travers une bibliothèque Pthread?

Supposons que nous ayons le bit de code suivant:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void guarantee(bool cond, const char *msg) {
    if (!cond) {
        fprintf(stderr, "%s", msg);
        exit(1);
    }
}

bool do_shutdown = false;   // Not volatile!
pthread_cond_t shutdown_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t shutdown_cond_mutex = PTHREAD_MUTEX_INITIALIZER;

/* Called in Thread 1. Intended behavior is to block until
trigger_shutdown() is called. */
void wait_for_shutdown_signal() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    while (!do_shutdown) {   // while loop guards against spurious wakeups
        res = pthread_cond_wait(&shutdown_cond, &shutdown_cond_mutex);
        guarantee(res == 0, "Could not wait for shutdown cond");
    }

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}

/* Called in Thread 2. */
void trigger_shutdown() {

    int res;

    res = pthread_mutex_lock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not lock shutdown cond mutex");

    do_shutdown = true;

    res = pthread_cond_signal(&shutdown_cond);
    guarantee(res == 0, "Could not signal shutdown cond");

    res = pthread_mutex_unlock(&shutdown_cond_mutex);
    guarantee(res == 0, "Could not unlock shutdown cond mutex");
}


2 commentaires

Oui, mais cela peut le faire si et seulement s'il n'y a pratiquement aucune manière légitime, la fonction de la bibliothèque pourrait modifier la valeur de la variable (par exemple s'il s'agit d'une variable automatique et son adresse n'est jamais prise).


@R: Correct ... et dans ce cas, il est en fait sûr de le faire, car aucun autre thread ne pouvait utiliser cette variable non plus.


4 Réponses :


6
votes

Bien sûr, les normes C et C ++ actuelles ne disent rien sur le sujet.

Autant que je sache, POSIX évite toujours de définir officiellement un modèle de concurrence (je peux être obsolète, cependant, auquel cas appliquez ma réponse uniquement aux versions POSIX précédentes). Par conséquent, ce que cela dit doit être lu avec un peu de sympathie - il ne pose pas précisément les exigences dans ce domaine, mais les entreprises devraient "savoir ce que cela signifie" et faire quelque chose qui fait des discussions utilisables.

Lorsque la norme indique que les mutexes "synchronisent l'accès à la mémoire", les implémentations doivent supposer que cela signifie que les modifications apportées sous la serrure dans un thread seront visibles sous la serrure d'autres threads. En d'autres termes, il est nécessaire (bien que pas suffisant) que les opérations de synchronisation incluent des barrières de mémoire d'un type ou d'un autre, et le comportement nécessaire d'une barrière de mémoire est qu'il doit assumer que les globaux peuvent changer.

Les threads ne peuvent pas être implémentés comme une bibliothèque < / a> Couvre que des problèmes spécifiques requis pour une PRHADDS soient réellement utilisables, mais ne sont pas explicitement énoncés dans la norme POSIX au moment de la rédaction (2004). Il devient très important de savoir si votre compilateur-écrivain ou qui a défini le modèle de mémoire pour votre mise en œuvre, accepte de BoehM ce que "utilisable" signifie, pour permettre au programmeur de "raisonner de manière convaincante quant à la correction du programme".

Notez que POSIX ne garantit pas de cache de mémoire cohérente, de sorte que si votre implémentation souhaite perversiement à cache do_something dans un registre dans votre code, alors même si vous l'avez marqué volatile , il pourrait parfois choisir de ne pas salaler le cache local de votre CPU entre l'opération de synchronisation et la lecture do_something . Donc, si l'écrivain thread est en cours d'exécution sur un processeur différent avec son propre cache, vous ne voyez peut-être pas le changement, même si.

C'est (une raison) Pourquoi les threads ne peuvent pas être mis en œuvre simplement comme une bibliothèque. Cette optimisation de la récupération d'un global volatilien uniquement du cache de la CPU locale serait valable dans une implémentation C à une seule filetage C [*], mais rompt le code multi-threadé. Par conséquent, le compilateur doit donc "savoir sur" les threads et comment ils affectent d'autres caractéristiques linguistiques (pour un exemple en dehors de Pthreads: sous Windows, où le cache est toujours cohérent, Microsoft sort de la sémantique supplémentaire qu'il accorde volatile dans le code multi-thread). Fondamentalement, vous devez supposer que si votre mise en œuvre s'est déroulée de fournir les fonctions Pthreads, cela passera à la peine de définir un modèle de mémoire fonctionnel dans lequel les serrures synchronisent réellement l'accès à la mémoire.

Si le compilateur peut aligner le fonctionner et prouver qu'il n'accède pas do_shutdown, puis peut-il mettre en cache do_shutdown même dans une multithread réglage? Qu'en est-il d'un non-inliné fonction dans la même unité de compilation?

Oui à tout cela - si l'objet est non volatile, le compilateur peut prouver que ce fil ne le modifie pas (par son nom ou via un pointeur aliasé), et si aucune barrière de mémoire ne se produit, alors Il peut réutiliser les valeurs précédentes. Il peut y avoir d'autres conditions spécifiques à la mise en œuvre qui l'arrêtent parfois, bien sûr.

[*] À condition que la mise en œuvre sait que le global ne soit pas situé à une adresse matérielle «spéciale» qui nécessite que les lectures suivent la mémoire cache dans la mémoire principale afin de voir les résultats de la puissance du matériel qui affecte cette adresse. Mais pour mettre un tel lieu de ce type ou pour rendre son emplacement spécial avec DMA ou autre chose, nécessite une magie spécifique à la mise en œuvre. Absent une telle magie la mise en œuvre en principe peut parfois le savoir.


17 commentaires

Je suis sûr de 99% que quelque part Posix définit le verrouillage et le déverrouillage de Mutex en tant que barrières de mémoire complètes, mais je ne peux pas le trouver maintenant.


Pthreads applique une barrière mémoire sur pthread_cond_wait () : Stackoverflow.com/questions/3208060/...


@Michael, @R ..: droite, il définit les fonctions "synchroniser la mémoire", mais cela ne définit pas ce que signifie réellement la "mémoire de synchroniser". Il y en a plus sur cela dans le papier de Boehm, tout ce que je dis, sera fondamentalement ma lecture (probablement imparfaite) de ses recherches. Il n'y a pas beaucoup de controverse ce que cela signifie, cela signifie quelles «barrières de mémoire» réellement sur le matériel connu, ainsi qu'un comportement complexe du compilateur pour garantir que les barrières ne sont pas subverties par certains types de réchérisation. Ce n'est tout simplement pas Posix qui dit cela, c'est le désir compréhensible des écrivains compilateurs de fournir des outils utiles.


Donc, le «si la norme n'interdit pas cela, il est autorisé« L'approche de la question gagne essentiellement, si un implauteur choisit de prendre cette ligne. Vous pouvez produire un tas de déchets, avec une synchronisation de données parfois non viable et affirmer plausiblement qu'elle est conforme à la lettre de la norme. Selon Boehm.


Je vais avoir une lecture étroite de ce papier. Au premier coup d'œil, on dirait que cela dit que Pthreads ne peut garantir que la sécurité du fil avec la coopération du compilateur.


@Michael: C'est certainement vrai et dit en outre que la spécification Pthreads ne définit pas de manière adéquate la coopération nécessaire au compilateur.


Juste pour être clair, l'OP demande ... cache une variable dans un registre dans un appel de bibliothèque Pthread? . Dans ce cas particulier, la qualification dans la question permet d'utiliser des barrières de mémoire animées. Aucune barrière de mémoire ne changera le contenu d'un registre.


@JOHNE: Je pense que le questionneur signifie: "Utilisez un registre pour mettre en cache une variable", pas "Cache une variable qui est déjà dans un registre". Je comprends que la question de savoir si le compilateur doit recharger à partir de la mémoire après l'appel, ou s'il peut continuer à utiliser la valeur qu'il a lu à partir de la mémoire plus tôt. Si la variable elle-même était marquée enregistrer qui changerait effectivement au jeu, car son adresse ne peut être prise, ce qui signifie qu'il ne peut pas être aliasé, ne peut pas être modifié par un autre code et Tout le problème disparaît. Mais dans la question do_shutdown n'est pas marqué enregistrer , c'est un global.


@Stevejessop " Si la variable elle-même était marquée enregistrement qui changerait effectivement le jeu, puisque son adresse ne peut être prise " Cela pourrait être pris en C ++. Quoi qu'il en soit, enregistrer change strictement rien, car le compilateur sait très bien si l'adresse d'une variable est prise ou non. Même si c'est global. En fait, son adresse n'est pas prise. En fait, c'est tout à fait sans importance.


@R " J'ai 59% sûr que quelque part Posix définit le verrouillage de mutex et le déverrouillage comme des barrières de mémoire complète " POSIX ne définit pas pthread _ fonctions en tant que barrières de mémoire CPU. POSIX définit l'effet en termes de lecture et d'écrit du programme, pas en trimestre de la CPU. Cette sous-analyse relie les niveaux.


@Stevejessop " Il dit que la spécification Pthreads ne définit pas de manière adéquate la coopération nécessaire du compilateur. " que de dire quelque chose de trivial: le thread de POSIX est une API standard pour les programmes, pas un ABI pour les compilateurs. . Il n'est pas prévu d'expliquer comment une mise en œuvre (compilateur + POSIX) devrait coopérer.


@Stevejessop " Vous pouvez produire une pile de déchets, avec une synchronisation de données parfois non viable et affirmer plausiblement qu'elle est conforme à la lettre de la norme. " Il est complètement évident qu'un compilateur qui utilise Une adresse mémoire fixe dans la convention d'appel de la fonction serait conforme et ne permettrait pas beaucoup de multi-threading.


" Si l'objet est non volatile et que le compilateur peut prouver que ce fil ne le modifie pas " Le compilateur ne peut évidemment pas prouver que cela.


@CuciousGuy: Je pense que vous devriez relire la partie de la question que je répondais ici. Cette partie de la question est: "Si le compilateur peut appeler un appel de fonction, et le prouver qu'il n'accède pas do_shutdown peut alors do_shutdown être mis en cache. Qu'en est-il d'une inlication fonction dans le même tu "? J'ai répondu "oui aux deux". Vous avez (peut-être accidentellement) affirmé qu'un compilateur ne peut jamais prouver si une fonction particulière dans le même TU modifie une mondiale particulière. C'est faux. Pour certaines fonctions, cela peut prouver exactement que, pour l'exemple le plus évident supposé que la fonction soit vide.


Je prétend que le compilateur ne peut éventuellement prouver que pour une fonction Pthread, ni pour une fonction de SYSCALL, ou appelle ou contient ASM. Sauf si vous mettez l'annotation pour permettre au compilateur de faire cela (voir Syntaxe ASM GCC).


@CuriousGuy: OK, mais ce n'est pas ce dont je parlais dans la partie de ma réponse que vous avez citée. Il y a beaucoup de fonctions pour lesquelles le compilateur peut le prouver, ce sont ceux que l'intervenant est intéressé. Il existe également de nombreuses fonctions pour lesquelles le compilateur ne peut pas le prouver - le plus notamment pour lequel ce n'est pas vrai, et La fonction fait modifie la variable. Mais aussi comme vous le dites, diverses conditions peuvent empêcher le compilateur de savoir quelle fonction une fonction.


La question initiale est " le compilateur pourrait savoir hypothétiquement que pthread_cond_wait () ne modifie pas do_shutdown. " donc j'ai compris que vous avez référé à des fonctions appelant directement ou indirectement pthread_cond_wait .



2
votes

Etant donné que do_shutdown a une liaison externe, le compilateur peut savoir ce qui se passe à travers l'appel (à moins que cela n'ait une visibilité complète des fonctions appelées). Il faudrait donc recharger la valeur (volatil ou non-filetage n'a aucune incidence sur cela) après l'appel.

Autant que je sache, rien ne dit rien directement à ce sujet dans la norme, sauf que la machine abstraite (à une seule filetée) La norme utilise pour définir le comportement des expressions indique que la variable doit être lue lorsqu'il est accessible dans une expression. La standard autorise la lecture de la variable à optimiser uniquement si le comportement peut être prouvé "comme si" était rechargé. Et cela ne peut arriver que si le compilateur peut savoir que la valeur n'a pas été modifiée par l'appel de la fonction.

Aussi pas que la bibliothèque Pthread ne garantit pas certaines garanties sur les barrières de mémoire pour diverses fonctions, y compris pthread_cond_wait () : Gardant une variable avec une garantie de Mutex Pthread, il n'est pas non plus caché?

maintenant, si do_shutdown était statique (pas de liaison externe) et que plusieurs threads utilisaient cette variable statique définie dans le même module (c'est-à-dire que l'adresse de la variable statique n'a jamais été prise à être transmis à un autre module), cela pourrait être une histoire différente. Par exemple, disons que vous avez une seule fonction qui utilise une telle variable et a commencé plusieurs instances de thread en cours d'exécution pour cette fonction. Dans ce cas, une implémentation de compilateur conformément aux normes pourrait mettre en cache la valeur entre les appels de fonction, car il pourrait supposer que rien d'autre ne pourrait modifier la valeur (le modèle de machine abstrait de la norme n'inclut pas le filetage).

Donc, dans ce cas, vous devriez utiliser des mécanismes pour vous assurer que la valeur a été rechargée à travers l'appel. Notez que, en raison des entraçabilité matérielles, le mot clé volatile peut ne pas être suffisant pour assurer une commande d'accès à la mémoire correcte - vous devriez compter sur des API fournies par Pthreads ou le système d'exploitation pour vous assurer que. (En tant que note latérale, les versions récentes des compilateurs de Microsoft do document que volatile appliquent des barrières de mémoire complètes, mais j'ai lu des opinions indiquant que cela n'est pas requis par la norme).


5 commentaires

Étant donné que la question est "tout compilateur conforme aux normes", cela inclut peut-être celui de l'ensemble de la bibliothèque standard, et toutes les dépendances, sont statiquement liés, ainsi que des optimisations de liaison / programme entiers, l'appel est entièrement visible? Peut-être pas un scénario réaliste.


C'est ce que dit le bon sens, malheureusement, il est difficile de savoir si elle est en quelque sorte appliquée / garantie par les normes.


Si le compilateur a des connaissances de programme entières, cela pourrait mettre en cache la valeur dans l'appel de la fonction s'il est déterminé que rien ne pourrait modifier la valeur. Mais la bibliothèque Pthreads est conçue pour empêcher la mise en cache sur un appel qui pourrait modifier une telle valeur (la bibliothèque peut avoir besoin d'effectuer quelque chose de non-standard / de mise en œuvre spécifique pour le faire, mais c'est le problème de la bibliothèque à résoudre).


" qui pourrait être une histoire différente. " S'il vous plaît montrer le code hypothétique pour cette histoire différente. " Une implémentation de compilateur conforme aux normes peut mettre en cache la valeur de la fonction d'appel ".


" Volatile Appliquez des obstacles à la mémoire complète, mais j'ai lu des opinions qui indiquent que cela n'est pas requis par la norme " Ce n'est pas simplement une "opinion". En général volatile n'a rien à voir avec MT. Pthreads Ne dis rien sur volatile .



0
votes

De mon propre travail, je peux dire que oui, le compilateur peut cacher des valeurs sur pthread_mutex_lock / pthread_mutex_unlock. J'ai passé la majeure partie du week-end à traquer un bug dans un peu de code causé par un ensemble de missions de pointeurs en cache et non disponibles aux threads qui les avaient besoin. En test rapide, j'ai enveloppé les affectations dans un verrouillage / déverrouillage mutex et les threads n'ont toujours pas accès aux valeurs de pointeur appropriées. Déplacer les assignations de pointeur et le verrouillage mutex associé à une fonction distincte résolvent le problème.


0 commentaires

2
votes

Les réponses ondulantes à la main sont tout faux. désolé d'être dur.

Il n'y a aucun moyen

Le compilateur pourrait savoir hypothétiquement que pthread_cond_wait () ne modifie pas do_shutdown.

Si vous croyez différemment, veuillez afficher une preuve: un programme complet C ++ tel qu'un compilateur non conçu pour MT pourrait déduire que pthread_cond_wait ne modifie pas do_shutdown

C'est absurde, un compilateur ne peut pas comprendre ce que font les fonctions pthread _ , sauf si elle a Connaissance intégrée des threads POSIX.


0 commentaires