2010-03-19 7 views
17

Je veux vérifier que ma compréhension est correcte. Ce genre de chose est difficile, donc je suis presque sûr qu'il me manque quelque chose. J'ai un programme composé d'un thread en temps réel et d'un thread en temps non réel. Je veux que le thread non RT puisse échanger un pointeur vers la mémoire utilisée par le thread RT.Échange atomique dans GNU C++

De la documentation, je crois comprendre que cela peut être accompli en g++ avec:

// global 
Data *rt_data; 

Data *swap_data(Data *new_data) 
{ 
#ifdef __GNUC__ 
    // Atomic pointer swap. 
    Data *old_d = __sync_lock_test_and_set(&rt_data, new_data); 
#else 
    // Non-atomic, cross your fingers.           
    Data *old_d = rt_data; 
    rt_data = new_data; 
#endif 
    return old_d; 
} 

C'est le seul endroit dans le programme (autre que la configuration initiale) où rt_data est modifié. Lorsque rt_data est utilisé dans le contexte temps réel, il est copié dans un pointeur local. Pour old_d, plus tard, lorsqu'il est certain que l'ancienne mémoire n'est pas utilisée, elle sera libérée dans le thread non RT. Est-ce correct? Ai-je besoin de volatile partout? Y a-t-il d'autres primitives de synchronisation que je devrais appeler?

Par la façon dont je fais cela en C++, bien que je suis intéressé à savoir si la réponse est différente pour C.

Merci à l'avance.

Répondre

24

Généralement, n'utilisez pas volatile lors de l'écriture de code concurrent dans C/C++.La sémantique de volatile est si proche de ce que vous voulez qu'elle est tentante mais à la fin volatile est not enough. Malheureusement Java/C# volatile != C/C++ volatile. Herb Sutter a un grand article expliquant le désordre confus.

Ce que vous voulez vraiment, c'est une barrière de mémoire. __sync_lock_test_and_set fournit la clôture pour vous.

Vous aurez également besoin d'un guide de mémoire lorsque vous copiez (chargez) le pointeur rt_data sur votre copie locale.

La programmation sans verrou est délicate. Si vous souhaitez utiliser les extensions C++ 0x de Gcc, c'est un peu plus simple:

#include <cstdatomic> 

std::atomic<Data*> rt_data; 

Data* swap_data(Data* new_data) 
{ 
    Data* old_data = rt_data.exchange(new_data); 
    assert(old_data != new_data); 
    return old_data; 
} 

void use_data() 
{ 
    Data* local = rt_data.load(); 
    /* ... */ 
} 
+0

Merci! Je peux suivre votre suggestion d'utiliser std :: atomic, c'est excellent. (Je ne suis pas encore familiarisé avec les dernières nouveautés en C++ 0x.) Juste par curiosité, si j'utilise __sync_lock_test_and_set, quelle est la bonne clôture à utiliser lors de la lecture? (c'est-à-dire, pour faire une copie locale) – Steve

3

Mise à jour: Cette réponse est incorrecte, car je manque le fait que volatile garantit que les accès aux volatile variables ne sont pas réorganisées, mais fournit aucune garantie à l'égard d'autres volatile non accès et manipulations. Une clôture de mémoire fournit de telles garanties et est nécessaire pour cette application. Ma réponse originale est ci-dessous, mais n'agis pas dessus. Voir this answer pour une bonne explication dans le trou dans ma compréhension qui a conduit à la réponse incorrecte suivante.

réponse originale:

Oui, vous avez besoin volatile sur votre déclaration rt_data; chaque fois qu'une variable peut être modifiée en dehors du flux de contrôle d'un thread qui y accède, elle doit être déclarée volatile. Bien que vous puissiez vous en sortir sans volatile puisque vous copiez sur un pointeur local, volatile aide au moins la documentation et empêche certaines optimisations du compilateur qui peuvent causer des problèmes. Prenons l'exemple suivant, adopté à partir DDJ:

volatile int a; 
int b; 
a = 1; 
b = a; 

S'il est possible d'avoir a sa valeur a changé entre a=1 et b=a, alors a devrait être déclarée volatile (à moins, bien sûr, l'attribution d'un out-of valeur de date à b est acceptable). Le multithreading, en particulier avec les primitives atomiques, constitue une telle situation. La situation est également déclenchée par des variables modifiées par des gestionnaires de signaux et par des variables mappées à des emplacements de mémoire impairs (par exemple des registres d'E/S de matériel). Voir aussi this question.

Sinon, ça me semble bien.

En C, j'utiliserais probablement les primitives atomiques fournies par GLib pour cela. Ils utiliseront une opération atomique là où elle est disponible et retomberont dans une implémentation lente mais correcte de mutex si les opérations atomiques ne sont pas disponibles. Boost peut fournir quelque chose de similaire pour C++.

+5

Les volatiles n'ont rien à voir avec la concurrence, elles sont complètement orthogonales. Ils traitent tous les deux de forcer le chargement/stockage et le réapprovisionnement, mais les garanties fournies par les volatiles ne résolvent pas les problèmes concurrents. –

+0

@Caspin Oui, volatile est orthogonal aux problèmes de simultanéité, et volatile seul ne suffit pas. Cependant, je crois comprendre que volatile est utile dans la programmation concurrente pour s'assurer que les threads voient les changements de l'autre. Il n'y a pas beaucoup de différence entre une variable qui est changée par un autre thread et celle qui est modifiée par une interruption matérielle - les deux transgressent les hypothèses nécessaires pour le réapprovisionnement load/store, et volatile indique au compilateur que ces suppositions ne tiennent pas nécessairement. –

+0

Orthogonal signifie que les problèmes de résolution volatile ne sont pas le genre de problèmes créés par la programmation simultanée. Une en particulier, la garantie de commande de volatile ne s'applique qu'au thread actuel et seulement par rapport aux autres volatiles. S'il vous plaît lire les liens fournis dans ma réponse que la description complète de volatile est trop complexe pour entrer dans un commentaire. Ou mieux encore demander à stackoverflow "Pourquoi le volatile n'est-il pas utile pour la programmation simultanée c/C++?" et je vais fournir une réponse en profondeur. –