9 votes

Un compilateur C/C++ peut-il légalement mettre en cache une variable dans un registre lors d'un appel à la bibliothèque pthread ?

Supposons que nous ayons le bout 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");
}

Un compilateur C/C++ conforme aux normes peut-il jamais mettre en cache la valeur de do_shutdown dans un registre à travers l'appel à pthread_cond_wait() ? Si non, quelles normes/clauses le garantissent ?

Le compilateur pourrait hypothétiquement savoir que pthread_cond_wait() ne modifie pas do_shutdown . Cela semble plutôt improbable, mais je ne connais pas de norme qui l'empêche.

En pratique, existe-t-il des compilateurs C/C++ qui mettent en cache la valeur de do_shutdown dans un registre à travers l'appel à pthread_cond_wait() ?

Quels appels de fonction le compilateur est-il assuré de ne pas mettre en cache la valeur de do_shutdown à travers ? Il est clair que si la fonction est déclarée en externe et que le compilateur ne peut pas accéder à sa définition, il ne doit faire aucune hypothèse sur son comportement et ne peut donc pas prouver qu'elle n'accède pas à do_shutdown . Si le compilateur peut mettre en ligne la fonction et prouver qu'elle n'accède pas à do_shutdown alors il peut cacher do_shutdown même dans un cadre multithread ? Qu'en est-il d'une fonction non-inlined dans la même unité de compilation ?

6voto

Steve Jessop Points 166970

Bien entendu, les normes actuelles du C et du C++ ne disent rien à ce sujet.

Pour autant que je sache, Posix évite toujours de définir formellement un modèle de concurrence (je suis peut-être dépassé, mais dans ce cas, appliquez ma réponse uniquement aux versions antérieures de Posix). Par conséquent, ce qu'il dit doit être lu avec un peu de sympathie - il ne définit pas précisément les exigences dans ce domaine, mais on attend des implémenteurs qu'ils "sachent ce que cela signifie" et fassent quelque chose qui rende les threads utilisables.

Lorsque la norme indique que les mutex "synchronisent l'accès à la mémoire", les implémentations doivent supposer que cela signifie que les changements effectués sous le verrou dans un thread seront visibles sous le verrou dans les autres threads. En d'autres termes, il est nécessaire (mais 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'elle doit supposer que les globaux peuvent changer.

Les threads ne peuvent pas être implémentés comme une bibliothèque couvre certaines questions spécifiques qui sont nécessaires pour qu'un pthreads soit réellement utilisable, mais qui ne sont pas explicitement énoncées dans la norme Posix au moment de la rédaction (2004). Il devient très important de savoir si votre compilateur-écrivain, ou celui qui a défini le modèle de mémoire pour votre implémentation, est d'accord avec Boehm sur ce que signifie "utilisable", en termes de permettre au programmeur de "raisonner de manière convaincante sur la correction du programme".

Notez que Posix ne garantit pas un cache mémoire cohérent, donc si votre implémentation veut perversement mettre en cache do_something dans un registre dans votre code, alors même si vous l'avez marqué volatile il pourrait choisir, de manière perverse, de ne pas salir le cache local de votre CPU entre l'opération de synchronisation et la lecture do_something . Ainsi, si le fil d'écriture est exécuté sur une autre unité centrale avec son propre cache, il se peut que vous ne voyiez pas le changement, même dans ce cas.

C'est (une des raisons) pour laquelle les threads ne peuvent pas être implémentés simplement comme une bibliothèque. Cette optimisation consistant à récupérer un global volatile uniquement dans le cache local du CPU serait valable dans une implémentation C monofilaire [*], mais casse le code multifilaire. Par conséquent, le compilateur doit "connaître" les threads et savoir comment ils affectent les autres caractéristiques du langage (pour un exemple en dehors de pthreads : sous Windows, où le cache est toujours cohérent, Microsoft explique clairement la sémantique supplémentaire qu'il accorde aux threads). volatile dans un code multithread). En gros, vous devez supposer que si votre implémentation s'est donné la peine de fournir les fonctions pthreads, alors elle se donnera la peine de définir un modèle de mémoire exploitable dans lequel les verrous synchronisent réellement l'accès à la mémoire.

Si le compilateur peut mettre en ligne le et prouver qu'elle n'accède pas à do_shutdown, alors il peut mettre en cache la fonction do_shutdown même dans un système multithreadé. multithread ? Qu'en est-il d'une fonction non inline dans la même unité de compilation ?

Oui à tout cela - si l'objet est non-volatile, et que le compilateur peut prouver que ce thread ne le modifie pas (soit par son nom, soit par un pointeur aliasé), et si aucune barrière mémoire ne se produit, alors il peut réutiliser les valeurs précédentes. Il peut y avoir et il y aura d'autres conditions spécifiques à l'implémentation qui l'arrêteront parfois, bien sûr.

[*] à condition que l'implémentation sache que le global n'est pas situé à une adresse matérielle "spéciale" qui nécessite que les lectures passent toujours par le cache vers la mémoire principale afin de voir les résultats de toute opération matérielle affectant cette adresse. Mais placer un global à un tel emplacement, ou rendre son emplacement spécial avec le DMA ou autre, nécessite une magie spécifique à l'implémentation. En l'absence d'une telle magie, l'implémentation peut en principe parfois le savoir.

2voto

curiousguy Points 2900

Les réponses à la main tendue sont toutes fausses. Désolé d'être dur.

Il n'y a aucun moyen

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

Si vous pensez le contraire, veuillez en apporter la preuve : un programme C++ complet tel qu'un compilateur non conçu pour MT pourrait en déduire que pthread_cond_wait ne modifie pas do_shutdown .

C'est absurde, un compilateur ne peut pas comprendre ce que pthread_ à moins qu'elle n'ait intégré connaissance des threads POSIX.

2voto

Michael Burr Points 181287

Depuis do_shutdown a un lien externe, il n'y a aucun moyen pour le compilateur de savoir ce qui lui arrive pendant l'appel (à moins qu'il ait une visibilité totale sur les fonctions appelées). Il devrait donc recharger la valeur (volatile ou non - le threading n'a aucune incidence sur ce point) après l'appel.

Pour autant que je sache, il n'y a rien de directement dit à ce sujet dans la norme, sauf que la machine abstraite (monofilaire) que la norme utilise pour définir le comportement des expressions indique que la variable doit être lue lorsqu'on y accède dans une expression. La norme permet que la lecture de la variable soit optimisée uniquement si l'on peut prouver que le comportement est "comme si" elle était rechargée. Et cela ne peut se produire que si le compilateur peut savoir que la valeur n'a pas été modifiée par l'appel de fonction.

Notez également que la bibliothèque pthread offre certaines garanties concernant les barrières de mémoire pour diverses fonctions, notamment pthread_cond_wait() : Le fait de garder une variable avec un mutex de pthread garantit-il qu'elle ne sera pas non plus mise en cache ?

Maintenant, si do_shutdown étaient statiques (sans lien externe) et que vous avez plusieurs threads qui utilisent 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 pour être transmise à 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 que vous avez lancé plusieurs instances de threads fonctionnant pour cette fonction. Dans ce cas, une implémentation de compilateur conforme aux normes pourrait mettre en cache la valeur entre les appels de fonction, car elle pourrait supposer que rien d'autre ne pourrait modifier la valeur (le modèle de machine abstraite de la norme n'inclut pas le threading).

Dans ce cas, il faudrait donc utiliser des mécanismes pour s'assurer que la valeur a été rechargée lors de l'appel. Notez qu'en raison des subtilités matérielles, l'élément volatile peut ne pas être suffisant pour assurer un ordre d'accès à la mémoire correct - vous devez compter sur les API fournies par pthreads ou le système d'exploitation pour s'en assurer. (à titre d'information, les versions récentes des compilateurs de Microsoft indiquent que le mot-clé volatile enforce des barrières de mémoire complètes, mais j'ai lu des avis qui indiquent que ce n'est pas requis par la norme).

0voto

Del Points 1

D'après mes propres travaux, je peux dire que oui, le compilateur peut mettre en cache des valeurs à travers pthread_mutex_lock/pthread_mutex_unlock. J'ai passé la majeure partie d'un week-end à tracer un bogue dans un bout de code qui était causé par un ensemble d'assignations de pointeurs mis en cache et non disponibles pour les threads qui en avaient besoin. Comme test rapide, j'ai enveloppé les assignations dans un mutex lock/unlock, et les threads n'avaient toujours pas accès aux valeurs correctes des pointeurs. Le déplacement des affectations de pointeurs et du verrouillage mutex associé vers une fonction distincte a permis de résoudre le problème.

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