Le "gare di dati" e le "condizioni di gara" sono effettivamente la stessa cosa nel contesto della programmazione concorrente


Risposte:


138

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.


"corsa dei dati (...) nessuna sincronizzazione che impone un ordine particolare tra questi accessi." Sono un po 'confuso. Nel tuo esempio, le operazioni potrebbero verificarsi in entrambi gli ordini (o = 1 quindi = 2 o viceversa). Perché non è una corsa ai dati?
josinalvo

5
@josinalvo: è un artefatto della definizione tecnica di una corsa di dati. Il punto chiave è che tra i due accessi, ci sarà uno sblocco e un acquisizione serratura (per uno dei possibili ordini). Per definizione, un rilascio del blocco e un acquisizione del blocco stabiliscono un ordine tra i due accessi, e quindi non c'è gara di dati.
Baris Kasikci

La sincronizzazione non richiede mai un ordine particolare tra le operazioni, quindi questo non è un modo molto fortunato per esprimerlo. D'altra parte, il JMM specifica che per ogni operazione di lettura deve esserci un'operazione di scrittura definita che osserva, anche in una corsa di dati. È difficile evitare di menzionare esplicitamente accade prima e ordine di sincronizzazione, ma anche la definizione JLS è sbagliata nel menzionare solo accade prima : per la sua definizione, due scritture volatili simultanee costituiscono una corsa di dati.
Marko Topolnik

@BarisKasikci "stabilisce un ordine" non ha alcun significato reale, per quanto mi riguarda. Sono solo parole da donnola. Onestamente non credo che la "corsa ai dati" sia un concetto remotamente utile, poiché letteralmente ogni posizione di memoria a cui si accede da più thread può essere considerata una "corsa ai dati"
Noldorin

Le coppie rilascio-acquisizione stabiliscono sempre un ordine. Una spiegazione generale è lunga, ma un esempio banale è una coppia segnale-attesa. @Noldorin "Stabilisce un ordine" si riferisce a un ordine accade prima, che è un concetto chiave della teoria della concorrenza (vedi l'articolo fondamentale di Lamport sulla relazione avvenuta prima) e dei sistemi distribuiti. Le gare di dati sono un concetto utile, in quanto la loro presenza pone molti problemi (ad esempio, semantica indefinita secondo il modello di memoria C ++, semantica molto complessa in Java ecc.). La loro individuazione ed eliminazione costituisce una vasta letteratura nella ricerca e nella pratica.
Baris Kasikci

20

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.


1
puoi inserire una riga nella tua risposta descrivendo cosa data racesignifica effettivamente in JLS?
Geek

@geek La parola "JLS" è un collegamento ipertestuale alla sezione pertinente di JLS.
Marko Topolnik

@MarkoTopolnik Sono un po 'confuso dall'esempio. Potresti spiegare: "Dato che sono volatile, non c'è gara di dati"? Voltility ha solo assicurato che sia visibile ma ancora: 1) non è sincronizzato e più thread possono leggere / scrivere contemporaneamente e 2) È condiviso campo non finale Quindi, secondo Java Concurrency in Practice (citato anche di seguito) , è data race e non race condition, non è vero?
aniliitb10

@ aniliitb10 Invece di fare affidamento su citazioni di seconda mano strappate dal loro contesto, dovresti rivedere la sezione 17.4 di JLS a cui ho collegato nella mia risposta. L'accesso a una variabile volatile è un'azione di sincronizzazione come definita in §17.4.2.
Marko Topolnik

@ aniliitb10 I Votaltiles non causano la corsa ai dati, perché i loro accessi possono essere ordinati. Cioè, puoi ragionare il loro ordine in questo o in quel modo, portando a risultati diversi. Con la data race, non hai modo di motivare l'ordine. Ad esempio, l'operazione i ++ di ogni thread può avvenire solo sul rispettivo valore i nella cache locale. A livello globale non hai modo di ordinare quelle operazioni (dal punto di vista del programmatore), a meno che tu non abbia un certo modello di memoria del linguaggio in atto.
Xiao-Feng Li,

3

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.


Nota che la domanda ha un tag indipendente dalla lingua.
martinkunev

1

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.