2010-03-19 11 views
17

Desidero verificare che la mia comprensione sia corretta. Questo genere di cose è complicato quindi sono quasi sicuro che mi manca qualcosa. Ho un programma composto da un thread in tempo reale e un thread non in tempo reale. Voglio che il thread non RT sia in grado di scambiare un puntatore con la memoria utilizzata dal thread RT.Swap atomico in GNU C++

Dalla documentazione, la mia comprensione è che questo può essere realizzato in g++ con:

// 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; 
} 

Questo è l'unico posto nel programma (diverso setup iniziale) dove rt_data viene modificato. Quando rt_data viene utilizzato nel contesto in tempo reale, viene copiato su un puntatore locale. Per old_d, in seguito, quando è sicuro che la vecchia memoria non viene utilizzata, verrà liberata nel thread non RT. È corretto? Ho bisogno di volatile ovunque? Ci sono altre primitive di sincronizzazione che dovrei chiamare?

Tra l'altro sto facendo questo in C++, anche se sono interessati a sapere se la risposta è diversa per C.

Grazie davanti a tempo.

risposta

24

Generalmente non utilizzare volatile quando si scrive codice simultaneo in C/C++.La semantica di volatile è così vicina a ciò che vuoi che sia allettante, ma alla fine volatile è not enough. Sfortunatamente Java/C# volatile != C/C++ volatile. Herb Sutter ha un ottimo article che spiega il caos confuso.

Quello che vuoi veramente è un recinto di memoria. __sync_lock_test_and_set fornisce la recinzione per te.

Avrete anche bisogno di una recinzione di memoria quando si copia (carica) il puntatore rt_data sulla propria copia locale.

La programmazione di blocco libero è complicata. Se sei disposto ad usare le estensioni C++ 0x di Gcc, è un po 'più semplice:

#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

Grazie! Potrei seguire il tuo suggerimento di usare std :: atomic, che è eccellente. (Non ho ancora familiarizzato con la più recente roba C++ 0x.) Solo per curiosità, se uso __sync_lock_test_and_set, qual è il recinto corretto da usare durante la lettura? (cioè, per fare una copia locale) – Steve

3

Aggiornamento: Questa risposta non è corretta, come mi manca il fatto che volatile garanzie che accede al volatile variabili non vengono riordinati, ma non fornisce tali garanzie rispetto ad altri non volatile accessi e manipolazioni. Una barriera di memoria fornisce tali garanzie ed è necessaria per questa applicazione. La mia risposta originale è sotto, ma non agire su di esso. Vedere this answer per una buona spiegazione nel buco nella mia comprensione che ha portato alla seguente risposta errata.

risposta originale:

Sì, è necessario volatile sulla vostra dichiarazione rt_data; ogni volta che una variabile può essere modificata al di fuori del flusso di controllo di un thread che lo accede, deve essere dichiarata volatile. Mentre è possibile allontanarsi senza volatile dato che si sta copiando su un puntatore locale, lo volatile aiuta almeno con la documentazione e impedisce anche alcune ottimizzazioni del compilatore che possono causare problemi. Si consideri il seguente esempio, adottato dal DDJ:

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

Se è possibile per a per avere il suo valore è cambiato tra a=1 e b=a, quindi a dovrebbe essere dichiarata volatile (a meno che, naturalmente, l'assegnazione di un out-of il valore della data a b è accettabile). Il multithreading, in particolare con i primitivi atomici, costituisce una situazione del genere. La situazione è anche innescata con variabili modificate dai gestori di segnale e da variabili mappate su posizioni di memoria dispari (ad esempio registri I/O hardware). Vedi anche this question.

Altrimenti, mi sembra soddisfacente.

In C, probabilmente utilizzerei le primitive atomiche fornite da GLib per questo. Utilizzeranno un'operazione atomica laddove disponibile e ricadranno su un'implementazione basata su mutex lenta ma corretta se le operazioni atomiche non sono disponibili. Boost potrebbe fornire qualcosa di simile per C++.

+5

Il volatile non ha nulla a che fare con la concorrenza, sono completamente ortogonali. Entrambi si occupano di forzare carico/immagazzinamento e riordino, ma le garanzie fornite da volatile non risolvono problemi concomitanti. –

+0

@Caspin Sì, volatile è ortogonale ai problemi di concorrenza e il solo volatile non è sufficiente. Tuttavia, è a mia conoscenza che volatile è utile nella programmazione concorrente per assicurarsi che i thread vedano le modifiche reciproche. Non c'è molta differenza tra una variabile cambiata da un altro thread e viene modificata da un interrupt hardware: entrambi violano le ipotesi necessarie per il riordino di carico/archivio e volatile dice al compilatore che tali ipotesi non necessariamente valgono. –

+0

Ortogonali significa che i problemi di risoluzione volatile non sono il tipo di problemi creati dalla programmazione concorrente. Uno in particolare, la garanzia di ordine di volatile si applica solo al thread corrente e solo rispetto ad altri volatili. Si prega di leggere i link forniti nella mia risposta in quanto l'intera descrizione di volatile è troppo complessa per rientrare in un commento. O meglio ancora chiedere StackOverflow "Perché non è volatile utile per la programmazione simultanea c/C++?" e fornirò una risposta approfondita. –