2009-07-09 4 views
8

Vengo in gran parte da uno sfondo C++, ma penso che questa domanda si applica al threading in qualsiasi lingua. Ecco lo scenario:In che modo i sistemi threaded fanno fronte ai dati condivisi che vengono memorizzati nella cache da cpus diversi?

  1. abbiamo due fili (ThreadA e ThreadB), e un valore x nella memoria condivisa

  2. supponga che l'accesso a x è opportunamente comandato da un mutex (o altro controllo di sincronizzazione adatto

  3. Se i thread si verificano su diversi processori, cosa succede se ThreadA esegue un'operazione di scrittura, ma il suo processore colloca il risultato nella sua cache L2 anziché nella memoria principale? Quindi, se ThreadB tenta di leggere il valore, non guarderà solo nella sua cache L1/L2/memoria principale e quindi lavorerà con qualunque valore precedente ci fosse?

Se non è questo il caso, come viene gestito questo problema?

Se questo è il caso, allora cosa si può fare al riguardo?

risposta

9

Il tuo esempio funzionerebbe perfettamente.

Più processori utilizzano uno coherency protocol come MESI per garantire che i dati rimangano sincronizzati tra le cache. Con MESI, ogni riga della cache viene considerata come modificata, mantenuta esclusivamente, condivisa tra CPU o non valida. La scrittura di una linea cache condivisa tra i processori lo costringe a diventare non valido nelle altre CPU, mantenendo sincronizzate le cache.

Tuttavia, questo non è abbastanza. Diversi processori hanno un diverso memory models e la maggior parte dei processori moderni supporta un certo livello di riordino degli accessi alla memoria. In questi casi, sono necessari memory barriers.

Per esempio se si dispone di Discussione A:

DoWork(); 
workDone = true; 

And Thread B:

while (!workDone) {} 
DoSomethingWithResults() 

Con entrambi in esecuzione su processori separati, non v'è alcuna garanzia che le scritture fatte entro DoWork() verrà essere visibili al thread B prima che la scrittura su workDone e DoSomethingWithResults() procedano con uno stato potenzialmente incoerente. Le barriere della memoria garantiscono un po 'di ordinamento delle letture e delle scritture - aggiungendo una barriera di memoria dopo DoWork() nel thread A forzerebbe tutte le letture/scritture fatte da DoWork a completare prima della scrittura su workDone, in modo che Thread B ottenga una visualizzazione coerente. I mutex forniscono intrinsecamente una barriera di memoria, in modo che le letture/scritture non possano passare una chiamata per bloccare e sbloccare.

Nel vostro caso, un processore segnalerebbe agli altri che esso ha sporcato una linea di cache e forza gli altri processori a ricaricarsi dalla memoria. L'acquisizione del mutex per leggere e scrivere il valore garantisce che la modifica della memoria sia visibile all'altro processore nell'ordine previsto.

+0

Grazie mille per questa risposta. Mi chiedevo se una sorta di meccanismo a livello di hardware dovesse entrare in gioco qui, perché sembrava che ci fossero dei limiti pratici su ciò che poteva essere realizzato a livello di linguaggio/compilatore. – csj

1

La maggior parte dei primitivi di blocco come i mutex implicano memory barriers. Questi impongono un flush della cache e si ricarica nuovamente.

Ad esempio,

ThreadA { 
    x = 5;   // probably writes to cache 
    unlock mutex; // forcibly writes local CPU cache to global memory 
} 
ThreadB { 
    lock mutex; // discards data in local cache 
    y = x;   // x must read from global memory 
} 
+1

Non credo che le barriere impongano un flush della cache, impongono vincoli sull'ordine delle operazioni di memoria. Un flush della cache non ti aiuterà se la scrittura su X può passare lo sblocco del mutex. – Michael

+0

Le barriere sarebbero piuttosto inutili se il compilatore riordina le operazioni di memoria su di esse, hmm? Almeno per GCC, penso che questo sia solitamente implementato con un clobber di memoria, che dice a GCC "invalidare qualsiasi ipotesi sulla memoria". – ephemient

+0

Oh, capisco cosa stai dicendo. Una svuotatura della cache non è necessaria, purché l'ordine sia correttamente rispettato tra i processori. Quindi suppongo che questa spiegazione sia una visione semplificata e la tua approfondisce maggiormente i dettagli dell'hardware. – ephemient

0

In generale, il compilatore riconosce memoria condivisa, e richiede un notevole sforzo per assicurare che la memoria condivisa è collocato in un luogo condivisibile. I compilatori moderni sono molto complicati nel modo in cui ordinano le operazioni e gli accessi alla memoria; tendono a comprendere la natura del threading e della memoria condivisa. Questo non vuol dire che siano perfetti, ma in generale, gran parte della preoccupazione è curata dal compilatore.

0

C# ha qualche build in supporto per questo tipo di problemi. È possibile contrassegnare una variabile con la parola chiave volatile, che ne consente la sincronizzazione su tutte le CPU.

public static volatile int loggedUsers; 

L'altra parte è un wrappper sintattica circa i metodi NET chiamati Threading.Monitor.Enter (x) e Threading.Monitor.Exit (x), dove x è una variabile per bloccare. Ciò fa sì che altri thread che tentano di bloccare x debbano attendere fino a quando il thread di blocco chiama Exit (x).

public list users; 
// In some function: 
System.Threading.Monitor.Enter(users); 
try { 
    // do something with users 
} 
finally { 
    System.Threading.Monitor.Exit(users); 
}