Trovo spesso che questi termini vengano utilizzati nel contesto della programmazione concorrente. Sono la stessa cosa o sono diversi?
Risposte:
No, non sono la stessa cosa. Non sono un sottoinsieme l'uno dell'altro. Inoltre non sono né la condizione necessaria né sufficiente l'una per l'altra.
La definizione di una corsa di dati è abbastanza chiara e, pertanto, la sua scoperta può essere automatizzata. Una corsa ai dati si verifica quando 2 istruzioni da thread diversi accedono alla stessa posizione di memoria, almeno uno di questi accessi è una scrittura e non c'è sincronizzazione che imponga un ordine particolare tra questi accessi.
Una condizione di competizione è un errore semantico. È un difetto che si verifica nella tempistica o nell'ordine degli eventi che porta a comportamenti errati del programma. Molte condizioni di gara possono essere causate da gare di dati, ma ciò non è necessario.
Considera il seguente semplice esempio in cui x è una variabile condivisa:
Thread 1 Thread 2
lock(l) lock(l)
x=1 x=2
unlock(l) unlock(l)
In questo esempio, le scritture su x dai thread 1 e 2 sono protette da blocchi, quindi avvengono sempre in un ordine imposto dall'ordine con cui i blocchi vengono acquisiti in fase di esecuzione. Cioè, l'atomicità delle scritte non può essere spezzata; c'è sempre un accade prima della relazione tra le due scritture in ogni esecuzione. Non possiamo proprio sapere quale scrittura avviene prima dell'altra a priori.
Non esiste un ordine fisso tra le scritture, perché i blocchi non possono fornire questo. Se la correttezza dei programmi è compromessa, ad esempio quando la scrittura in x dal thread 2 è seguita dalla scrittura in x nel thread 1, diciamo che c'è una condizione di competizione, anche se tecnicamente non c'è gara di dati.
È molto più utile rilevare le condizioni di gara rispetto alle gare di dati; tuttavia questo è anche molto difficile da ottenere.
Anche costruire l'esempio inverso è banale. Questo post del blog spiega anche molto bene la differenza, con un semplice esempio di transazione bancaria.
Secondo Wikipedia, il termine "race condition" è in uso sin dai tempi delle prime porte logiche elettroniche. Nel contesto di Java, una condizione di competizione può riguardare qualsiasi risorsa, come un file, una connessione di rete, un thread da un pool di thread, ecc.
Il termine "gara di dati" è meglio riservare al suo significato specifico definito dal JLS .
Il caso più interessante è una condizione di gara che è molto simile a una gara di dati, ma ancora non lo è, come in questo semplice esempio:
class Race {
static volatile int i;
static int uniqueInt() { return i++; }
}
Poiché i
è volatile, non vi è alcuna corsa di dati; tuttavia, dal punto di vista della correttezza del programma esiste una race condition dovuta alla non atomicità delle due operazioni: lettura i
, scrittura i+1
. Più thread possono ricevere lo stesso valore da uniqueInt
.
data race
significa effettivamente in JLS?
No, sono diversi e nessuno dei due è un sottoinsieme di uno o viceversa.
Il termine race condition viene spesso confuso con il termine correlato data race, che si verifica quando la sincronizzazione non viene utilizzata per coordinare tutti gli accessi a un campo non finale condiviso. Si rischia una corsa di dati ogni volta che un thread scrive una variabile che potrebbe essere letta successivamente da un altro thread o legge una variabile che potrebbe essere stata scritta l'ultima volta da un altro thread se entrambi i thread non utilizzano la sincronizzazione; il codice con gare di dati non ha una semantica definita utile nel modello di memoria Java. Non tutte le race condition sono gare di dati e non tutte le gare di dati sono race condition, ma entrambe possono causare il malfunzionamento di programmi concorrenti in modi imprevedibili.
Tratto dall'eccellente libro - Java Concurrency in Practice di Joshua Bloch & Co.
TL; DR: La distinzione tra data race e race condition dipende dalla natura della formulazione del problema e da dove tracciare il confine tra comportamento indefinito e comportamento ben definito ma indeterminato. L'attuale distinzione è convenzionale e riflette al meglio l'interfaccia tra l'architetto del processore e il linguaggio di programmazione.
1. Semantica
Data race si riferisce specificamente agli "accessi alla memoria" (o azioni o operazioni) in conflitto non sincronizzati alla stessa posizione di memoria. Se non c'è conflitto negli accessi alla memoria, mentre c'è ancora un comportamento indeterminato causato dall'ordinamento delle operazioni, questa è una condizione di competizione.
Nota "accessi alla memoria" qui hanno un significato specifico. Si riferiscono al carico di memoria "puro" o alle azioni di memorizzazione, senza alcuna semantica aggiuntiva applicata. Ad esempio, un archivio di memoria da un thread non sa (necessariamente) quanto tempo impiega i dati per essere scritti nella memoria e infine si propaga a un altro thread. Per un altro esempio, un archivio di memoria in una posizione prima di un altro archivio in un'altra posizione dallo stesso thread non garantisce (necessariamente) che i primi dati scritti nella memoria siano prima del secondo. Di conseguenza, l'ordine di quegli accessi alla memoria pura non è (necessariamente) in grado di essere "ragionato" , e qualsiasi cosa potrebbe accadere, se non diversamente ben definito.
Quando gli "accessi alla memoria" sono ben definiti in termini di ordinamento tramite sincronizzazione, semantiche aggiuntive possono garantire che, anche se i tempi degli accessi alla memoria sono indeterminati, il loro ordine può essere "ragionato" attraverso le sincronizzazioni. Nota, sebbene l'ordine tra gli accessi alla memoria possa essere ragionato, non sono necessariamente determinati, da qui la condizione di gara.
2. Perché la differenza?
Ma se l'ordine è ancora indeterminato in race condition, perché preoccuparsi di distinguerlo dalla data race? Il motivo è pratico piuttosto che teorico. È perché esiste la distinzione nell'interfaccia tra il linguaggio di programmazione e l'architettura del processore.
Un'istruzione di caricamento / memorizzazione della memoria nell'architettura moderna viene solitamente implementata come accesso "puro" alla memoria, a causa della natura della pipeline fuori ordine, della speculazione, della cache multilivello, dell'interconnessione cpu-ram, in particolare multi-core, ecc. Ci sono molti fattori che portano a tempi e ordini indeterminati. Per imporre l'ordinamento per ogni istruzione di memoria incorre in enormi penalità, specialmente in un design del processore che supporta multi-core. Quindi la semantica di ordinamento viene fornita con istruzioni aggiuntive come varie barriere (o recinzioni).
La corsa dei dati è la situazione dell'esecuzione delle istruzioni del processore senza recinzioni aggiuntive per aiutare a ragionare sull'ordinamento degli accessi alla memoria in conflitto. Il risultato non è solo indeterminato, ma anche forse molto strano, ad esempio, due scritture nella stessa posizione di parola da thread diversi possono risultare con ciascuna metà della scrittura della parola, o possono operare solo sui loro valori memorizzati nella cache locale. - Questi sono comportamenti indefiniti, dal punto di vista del programmatore. Ma sono (di solito) ben definiti dal punto di vista dell'architetto del processore.
I programmatori devono avere un modo per motivare la loro esecuzione del codice. La corsa ai dati è qualcosa che non possono avere senso, quindi dovrebbero sempre evitare (normalmente). Questo è il motivo per cui le specifiche del linguaggio che sono di livello sufficientemente basso di solito definiscono la corsa dei dati come un comportamento indefinito, diverso dal comportamento della memoria ben definito della condizione di competizione.
3. Modelli di memoria linguistica
Processori diversi possono avere un comportamento di accesso alla memoria diverso, ad esempio il modello di memoria del processore. È scomodo per i programmatori studiare il modello di memoria di ogni processore moderno e quindi sviluppare programmi che possono trarne vantaggio. È auspicabile che il linguaggio possa definire un modello di memoria in modo che i programmi di quel linguaggio si comportino sempre come previsto come definito dal modello di memoria. Ecco perché Java e C ++ hanno i loro modelli di memoria definiti. È compito degli sviluppatori del compilatore / runtime garantire che i modelli di memoria del linguaggio siano applicati su architetture di processori differenti.
Detto questo, se un linguaggio non vuole esporre il comportamento di basso livello del processore (ed è disposto a sacrificare alcuni vantaggi prestazionali delle architetture moderne), può scegliere di definire un modello di memoria che nasconda completamente i dettagli "puri" accede alla memoria, ma applica la semantica di ordinamento per tutte le loro operazioni di memoria. Quindi gli sviluppatori del compilatore / runtime possono scegliere di trattare ogni variabile di memoria come volatile in tutte le architetture del processore. Per questi linguaggi (che supportano la memoria condivisa tra i thread), non ci sono gare di dati, ma possono ancora esserci condizioni di gara, anche con un linguaggio di completa coerenza sequenziale.
D'altra parte, il modello di memoria del processore può essere più rigoroso (o meno rilassato, o di livello superiore), ad esempio, implementando la consistenza sequenziale come faceva il processore dei primi tempi. Quindi tutte le operazioni di memoria vengono ordinate e non esiste alcuna competizione di dati per i linguaggi in esecuzione nel processore.
4. Conclusione
Tornando alla domanda originale, IMHO va bene definire la corsa dei dati come un caso speciale di condizione di gara, e la condizione di razza a un livello può diventare una corsa di dati a un livello superiore. Dipende dalla natura della formulazione del problema e da dove tracciare il confine tra comportamento indefinito e comportamento ben definito ma indeterminato. Solo la convenzione corrente definisce il confine all'interfaccia del processore del linguaggio, non significa necessariamente che sia sempre e debba essere così; ma la convenzione corrente probabilmente riflette meglio l'interfaccia (e la saggezza) all'avanguardia tra l'architetto del processore e il linguaggio di programmazione.