2009-08-03 14 views
39

Diciamo solo che hai una semplice operazione che gira su un thread in background. Si desidera fornire un modo per annullare questa operazione in modo da creare un flag booleano impostato su true dal gestore di eventi click di un pulsante di annullamento.Devo bloccare o contrassegnare come volatile quando si accede a un semplice flag booleano in C#?

private bool _cancelled; 

private void CancelButton_Click(Object sender ClickEventArgs e) 
{ 
    _cancelled = true; 
} 

Ora stai impostando il flag di annullamento dal thread della GUI, ma lo stai leggendo dal thread in background. Hai bisogno di bloccare prima di accedere al bool?

avresti bisogno per fare questo (e bloccare ovviamente nel gestore di eventi click tasto troppo):

while(operationNotComplete) 
{ 
    // Do complex operation 

    lock(_lockObject) 
    { 
     if(_cancelled) 
     { 
      break; 
     } 
    } 
} 

O è accettabile per fare questo (senza serratura):

while(!_cancelled & operationNotComplete) 
{ 
    // Do complex operation 
} 

O che dire della variabile _cancelled come volatile. È necessario?

[So che esiste la classe BackgroundWorker con il suo metodo CancelAsync() integrato, ma mi interessa la semantica e l'uso di accesso variabile e con accesso variabile ai thread qui, non l'implementazione specifica, il codice è solo un esempio. ]

Sembra esserci due teorie.

1) Perché è un semplice tipo integrato (e l'accesso ai tipi incorporati è atomico in .net) e poiché ci stiamo solo scrivendo in un posto e solo leggendo sul thread in background non è necessario bloccare o segna come volatile.
2) Dovresti contrassegnarlo come volatile perché se non lo fai il compilatore può ottimizzare la lettura del ciclo while perché non pensa nulla che sia in grado di modificare il valore.

Qual è la tecnica corretta? (E perché?)

[Modifica: Sembra che ci siano due scuole di pensiero chiaramente definite e opposte su questo. Sto cercando una risposta definitiva su questo quindi per favore se possibile postare le tue ragioni e citare le tue fonti insieme alla tua risposta.]

+6

Sì, è * necessario * bisogno di 'volatile' o' lock' (per fungere da barriera di memoria): vedere http://stackoverflow.com/questions/458173/can-ac-thread-really-cache- valore-e-ignora-cambia-a-quel-valore-su-altro-th/458193 # 458193 –

+1

Ciò significa che Simon sarà lasciato rosso di fronte al dibattito di massa che ha avuto con EFraim (http : //stackoverflow.com/questions/1221839/c-break-out-of-loop-on-button-press/1221854#1221854) – ThePower

+1

@Marc: Bel esempio, grazie. @ThePower: Sono felice di alzare le mani quando non ne sono sicuro, quindi spero di evadere solo leggermente di rosa = :) –

risposta

31

In primo luogo, la filettatura è difficile ;-p

Sì, nonostante tutte le voci al contrario, esso è tenuto a sia uso lockovolatile (ma non entrambi) quando si accede un bool da più thread.

Per i tipi e gli accessi semplici come un flag di uscita (bool), quindi volatile è sufficiente - questo garantisce che i thread non memorizzino il valore nei loro registri (ovvero: uno dei thread non vede mai gli aggiornamenti).

Per valori più grandi (dove atomicità è un problema), o dove si desidera sincronizzare un successione delle operazioni (un tipico esempio di essere "se non esiste e aggiungere" accesso dizionario), un lock è più versatile. Questo funge da barriera di memoria, quindi ti dà ancora la sicurezza del thread, ma fornisce altre funzionalità come pulse/wait. Si noti che non si dovrebbe usare un lock su un tipo di valore o un string; né Type o this; l'opzione migliore è avere il proprio oggetto di blocco come campo (readonly object syncLock = new object();) e bloccarlo.

Per un esempio di come si interrompe (ad esempio, il ciclo per sempre) se non si sincronizza - see here.

Per estendere più programmi, una primitiva del sistema operativo come o *ResetEvent può anche essere utile, ma questo è eccessivo per un singolo exe.

+0

Penso che questo risolva il problema.Un esempio riproducibile di come può andare storto se non usi lock o volatile. –

+0

@ MarcGravell ♦ ci sono possibilità che 2 thread scrivano sulla variabile volatile esattamente nello stesso momento? – onmyway133

+0

@entropy sì, non c'è niente che impedisca tutto ciò. Uno dei valori vincerà; non è definito quale. Nota che il compilatore ti permette solo di utilizzare 'volatile' con tipi che possono essere scritti atomicamente, così non ti ritroverai con un valore lacerato –

6

_cancelled deve essere volatile. (se non si sceglie di bloccare)

Se un thread modifica il valore di _cancelled, altri thread potrebbero non visualizzare il risultato aggiornato.

Inoltre, penso che le operazioni di lettura/scrittura di _cancelled sono atomica:

Sezione 12.6.6 degli stati CLI spec: "Un CLI conforme garantisce che in lettura e scrittura a correttamente posizioni di memoria allineate non più grandi rispetto alla dimensione nativa della parola è atomica quando tutti gli accessi in scrittura a una posizione hanno la stessa dimensione. "

+0

@Mitch: ma in questo caso questo significa che può rovinare tutto. – EFraim

+0

Proprio come un punto di correzione, il blocco non ha alcun effetto sul fatto che una lettura o scrittura su una variabile sia memorizzata nella cache. Il blocco NON sostituisce una variabile volatile. –

+0

l'atomicità ha poco a che fare con esso ... se l'altro thread sta ancora masticando i propri registri allora fa la differenza zero se si scrive il valore in 1 chunk o in 26. –

2

Per la sincronizzazione dei thread, si consiglia di utilizzare una delle classi EventWaitHandle, ad esempio ManualResetEvent. Mentre è marginalmente più semplice utilizzare una semplice bandiera booleana come fai qui (e sì, dovresti contrassegnarla come volatile), IMO è meglio entrare nella pratica di usare gli strumenti di threading.Per i vostri scopi, si farebbe qualcosa di simile ...

private System.Threading.ManualResetEvent threadStop; 

void StartThread() 
{ 
    // do your setup 

    // instantiate it unset 
    threadStop = new System.Threading.ManualResetEvent(false); 

    // start the thread 
} 

nel tuo thread ..

while(!threadStop.WaitOne(0) && !operationComplete) 
{ 
    // work 
} 

Poi nella GUI di cancellare ...

threadStop.Set(); 
+0

Una primitiva del sistema operativo è solitamente eccessiva per una singola app, IMO. La maggior parte delle cose si può fare con un semplice "monitor". –

+0

@Marc: Anche se un * ResetEvent potrebbe non essere esplicitamente richiesto qui, non credo che sarei disposto a dire che è eccessivo per una singola applicazione. Esistono molti casi in cui potrebbero essere necessari più thread all'interno di un'applicazione per utilizzare la funzionalità di EventWaitHandle come ManualResetEvent. –

1

sguardo su Interlocked.Exchange(). Fa una copia molto veloce in una variabile locale che può essere utilizzata per il confronto. È più veloce di lock().

+0

Non sono le considerazioni sulla velocità; è la correttezza ... –

+0

+1, non sempre la migliore ma è una delle tre possibilità (bloccate/bloccate/volatili). –

+0

Interlocked.Exchange() non sarebbe la mia prima scelta per una variabile usata per controllare un loop. Userei un evento/WaitHandle. Se devi ottenere atomicamente una singola variabile intera/booleana, è una buona scelta. – hughdbrown

5

Il blocco non è richiesto perché si dispone di uno scenario di singolo writer e un campo booleano è una struttura semplice senza il rischio di corrompere lo stato (while it was possible to get a boolean value that is neither false nor true). Ma è necessario contrassegnare il campo come volatile per impedire al compilatore di eseguire alcune ottimizzazioni. Senza il modificatore volatile il compilatore poteva memorizzare il valore in un registro durante l'esecuzione del ciclo sul proprio thread di lavoro e, a sua volta, il ciclo non riconosceva mai il valore modificato. Questo articolo MSDN (How to: Create and Terminate Threads (C# Programming Guide)) risolve questo problema. Mentre è necessario il blocco, un blocco avrà lo stesso effetto del campo volatile.

+0

Esatto. Dal momento che leggi o imposti il ​​valore di questo flag, tutto ciò di cui hai bisogno è un attributo 'volatile'. – jpbochi

+0

Ma in teoria si potrebbe avere uno scenario piuttosto noioso se l'aggiornamento del campo booleano non è atomico e un aggiornamento parziale risulta in un valore che non è né vero né falso, ma ciò non causerebbe problemi nello scenario d'uso dato. –

+0

@Daniel - un aggiornamento bool è garantito per essere atomico dalle specifiche. –

Problemi correlati