2013-10-25 12 views
41

Basta controllare ... _count si accede in sicurezza, giusto?Questo è thread-safe giusto?

Entrambi i metodi sono accessibili da più thread.

private int _count; 

public void CheckForWork() { 
    if (_count >= MAXIMUM) return; 
    Interlocked.Increment(ref _count); 
    Task t = Task.Run(() => Work()); 
    t.ContinueWith(CompletedWorkHandler); 
} 

public void CompletedWorkHandler(Task completedTask) { 
    Interlocked.Decrement(ref _count); 
    // Handle errors, etc... 
} 
+31

Letteralmente "BWAHAHAHAHAH!" al titolo qui. –

+2

Voglio solo sottolineare, questo è un modo piuttosto brutto per implementare ciò che è essenzialmente una Task Factory con un grado massimo di parallelismo. Ci sono tutta una serie di problemi, sia dal punto di vista del design (immagino che CheckForWork() venga chiamato su un timer e nel codice "...", ma questo è un problema. Implementazione del pool di thread che sta vanificando molto di ciò che si sta tentando di fare) e l'implementazione (Il problema più ovvio: se Work() genera un'eccezione o qualcuno si dimentica di chiamare CompletedWorkHandler() si affama la coda di lavoro) . – Chuu

risposta

41

No, if (_count >= MAXIMUM) return; non è sicuro.

edit: Avresti per chiudere intorno anche la lettura, che dovrebbe quindi logicamente essere raggruppato con l'incremento, quindi mi piacerebbe riscrivere come

private int _count; 

private readonly Object _locker_ = new Object(); 

public void CheckForWork() { 
    lock(_locker_) 
    { 
     if (_count >= MAXIMUM) 
      return; 
     _count++; 
    } 
    Task.Run(() => Work()); 
} 

public void CompletedWorkHandler() { 
    lock(_locker_) 
    { 
     _count--; 
    } 
    ... 
} 
97

Questo è thread-safe, giusto?

Supponiamo che MASSIMO sia uno, il conteggio è zero e cinque thread chiamano CheckForWork.

Tutti e cinque i thread potrebbero verificare che il conteggio sia inferiore a MASSIMO. Il contatore verrebbe quindi espulso fino a cinque e cinque inizi.

Ciò sembra contrario all'intenzione del codice.

Inoltre: il campo non è volatile. Quindi quale meccanismo garantisce che qualsiasi thread legga un valore aggiornato sul percorso di barriera senza memoria? Niente lo garantisce! Crei una barriera di memoria solo se la condizione è falsa.

Più in generale: stai facendo una falsa economia qui. Procedendo con una soluzione a basso numero di blocchi si risparmiano le decine di nanosecondi che sarebbero stati necessari per il blocco non mirato. Basta prendere il lucchetto. Puoi permetterti una dozzina di nanosecondi in più.

E ancora, più in generale: non scrivere codice di basso di blocco a meno che non sei un esperto di architetture di processori e sapere tutte le ottimizzazioni che una CPU è consentito di eseguire su percorsi a bassa-lock. Non sei un esperto. Nemmeno io. Ecco perché Non scrivere il codice di blocco basso.

+0

Non so nemmeno cosa intendi per codice low-lock. Quello che stavo cercando di realizzare era limitare in qualche modo il numero di lavori simultanei che venivano fatti. Ho pensato di leggere i tipi di valore in cui thread-safe. – Unlimited071

+2

@ Unlimited071 Che [dipende da cosa intendi per thread safe] (http://blogs.msdn.com/b/ericlippert/archive/2009/10/19/what-is-this-thing-you-call-thread -safe.aspx). Alcune operazioni possono essere eseguite senza serrature e alcune no. A meno che tu non sappia cosa stai facendo e come ti aspetti che sia fatto, non puoi davvero parlare se il programma è "sicuro" o meno. Il codice può essere sicuro se utilizzato in una determinata situazione ed essere pericoloso se utilizzato in un'altra. Niente è mai interamente thread save indipendentemente dal modo in cui viene utilizzato. – Servy

+0

@ Unlimited071: Quindi non dovresti scrivere codice di blocco basso! Il codice low-lock è ciò che stai scrivendo: codice che cerca di essere protetto da un thread senza prendere alcun blocco. Non so cosa intendi per "tentare di limitare il numero di lavori simultanei che si stanno facendo", ma suona sicuramente come "cercare di evitare di prendere le serrature" per me. –

4

Definire il thread sicuro.

Se si desidera assicurarsi che _count non sia mai maggiore di MAXIMUM, allora non è stato possibile.

Che cosa si dovrebbe fare è bloccare intorno a quella troppo:

private int _count; 
private object locker = new object(); 

public void CheckForWork() 
{ 
    lock(locker) 
    { 
     if (_count >= MAXIMUM) return; 
     _count++; 
    } 
    Task.Run(() => Work()); 
} 

public void CompletedWorkHandler() 
{ 
    lock(locker) 
    { 
     _count--; 
    } 
    ... 
} 

Si potrebbe anche voler dare un'occhiata a The SemaphoreSlim di classe.

+1

Grazie per il collegamento alla classe SemaphoreSlim, sembra che potrebbe aiutare con quello che sto cercando di realizzare – Unlimited071

20

No, quello che hai non è sicuro. Il controllo per vedere se _count >= MAXIMUM potrebbe correre con la chiamata a Interlocked.Increment da un altro thread. Questo è in realtà davvero difficile da risolvere utilizzando tecniche di blocco basso. Per fare in modo che funzioni correttamente, è necessario eseguire una serie di operazioni diverse in modo atomico senza utilizzare un lucchetto. Questa è la parte difficile. La serie di operazioni in questione sono:

  • Leggi _count
  • prova _count >= MAXIMUM
  • prendere una decisione basata su quanto sopra.
  • Incremento _count in base alla decisione presa.

Se non fai apparire tutti e 4 questi passaggi atomici, allora ci sarà una condizione di gara. Il modello standard per eseguire un'operazione complessa senza prendere un blocco è il seguente.

public static T InterlockedOperation<T>(ref T location) 
{ 
    T initial, computed; 
    do 
    { 
    initial = location; 
    computed = op(initial); // where op() represents the operation 
    } 
    while (Interlocked.CompareExchange(ref location, computed, initial) != initial); 
    return computed; 
} 

Avviso che cosa sta succedendo. L'operazione viene ripetutamente eseguita fino a quando l'operazione ICX determina che il valore iniziale non è cambiato tra il momento in cui è stato letto per la prima volta e il momento in cui è stato effettuato il tentativo di cambiarlo. Questo è il modello standard e la magia si verifica tutto a causa della chiamata CompareExchange (ICX). Si noti, tuttavia, che ciò non tiene conto dello ABA problem.

Che cosa si potrebbe fare:

Quindi, prendendo il modello di cui sopra e inserirla nel codice si tradurrebbe in questo.

public void CheckForWork() 
{ 
    int initial, computed; 
    do 
    { 
     initial = _count; 
     computed = initial < MAXIMUM ? initial + 1 : initial; 
    } 
    while (Interlocked.CompareExchange(ref _count, computed, initial) != initial); 
    if (replacement > initial) 
    { 
     Task.Run(() => Work()); 
    } 
} 

Personalmente, vorrei punt sulla strategia a basso serratura del tutto. Ci sono diversi problemi con quello che ho presentato sopra.

  • Questo potrebbe effettivamente essere più lento di un blocco rigido. Le ragioni sono difficili da spiegare e al di fuori della portata della mia risposta.
  • Qualsiasi deviazione da quanto sopra può causare il fallimento del codice. Sì, è davvero così fragile.
  • È difficile da capire. Voglio dire guardalo. È brutto.

Quello che dovrebbe fare:

Andando con il percorso di blocco duro il codice potrebbe essere simile.

private object _lock = new object(); 
private int _count; 

public void CheckForWork() 
{ 
    lock (_lock) 
    { 
    if (_count >= MAXIMUM) return; 
    _count++; 
    } 
    Task.Run(() => Work()); 
} 

public void CompletedWorkHandler() 
{ 
    lock (_lock) 
    { 
    _count--; 
    } 
} 

Si noti che questo è molto più semplice e considerevolmente meno incline agli errori. Si può effettivamente scoprire che questo approccio (hard lock) è in realtà più veloce di quello che ho mostrato sopra (low lock). Di nuovo, la ragione è complicata e ci sono tecniche che possono essere utilizzate per accelerare le cose, ma al di fuori dello scopo di questa risposta.


Il problema ABA non è realmente un problema in questo caso perché la logica non dipende _count rimanendo invariata. Importa solo che il suo valore è lo stesso in due punti nel tempo, indipendentemente da quello che è successo nel mezzo. In altre parole, il problema può essere ridotto a uno in cui è sembrava come il valore non è cambiato anche se in realtà potrebbe avere.

+0

Beh, avrei dovuto omettere Task.Run. Quello che ho cercato di dire è che il metodo era fare la coda per un lavoro che sto cercando di limitare. – Unlimited071

36

Questo è quello che Semaphore e SemaphoreSlim sono per:

private readonly SemaphoreSlim WorkSem = new SemaphoreSlim(Maximum); 

public void CheckForWork() { 
    if (!WorkSem.Wait(0)) return; 
    Task.Run(() => Work()); 
} 

public void CompletedWorkHandler() { 
    WorkSem.Release(); 
    ... 
} 
0

è possibile effettuare le seguenti operazioni se non si desidera bloccare o passare a un semaforo:

if (_count >= MAXIMUM) return; // not necessary but handy as early return 
if(Interlocked.Increment(ref _count)>=MAXIMUM+1) 
{ 
    Interlocked.Decrement(ref _count);//restore old value 
    return; 
} 
Task.Run(() => Work()); 

Incremento restituisce il valore incrementato su cui puoi ricontrollare se _count è inferiore al massimo, se il test fallisce allora ripristino il vecchio valore

+1

Il tuo codice consente di attivare il blocco live, in cui 40 thread sono sempre compresi tra quelli che si sono incrementati troppo e che sono stati risolti decrescendo. Nessun lavoro può essere svolto mentre questo è il caso, perché si ottengono falsi positivi sul "c'è troppo lavoro?". –

+0

@Strilanc il decremento nel blocco if si ridurrà a MAXIMUM in modo che si verifichi il ritorno anticipato, anche i primi thread MAXIMUM saranno in grado di funzionare, e quando viene eseguito un thread, un incremento restituirà MAXIMUM-1 per alcuni thread permettergli di correre, accanto a livelock è sempre un pericolo quando si sceglie per l'atomica. –

+3

Strilanc ha ragione. Questo è veramente difficile da visualizzare. Lasciami provare a spiegare un modo diverso. Immagina 400 thread tutti in competizione per il tandem di incremento e decremento. Ora, supponiamo che ci sia una probabilità del 10% che qualsiasi thread sarà tra le due chiamate in un dato momento. Se la distribuzione e le probabilità rimangono costanti (che potrebbero verificarsi sotto un carico pesante), allora deduciamo che ci sono sempre 400 * 0.1 = 40 fili in questo stato semi-cotto. Se 40 thread sono sempre in questo stato, allora '_count' deve essere> = 40 in ogni momento, anche se non si sta facendo attualmente alcun lavoro. Live locked! –

Problemi correlati