2010-04-29 14 views
5

Ho avuto il seguente codice nella mia applicazione per alcuni anni e non ho mai visto un problema da esso.Code e maniglie di attesa in C#

while ((PendingOrders.Count > 0) || (WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 
     } 
    } 

    // Do Some Work if fbo is != null 
} 

Dove CommandEventArr è costituito dalla NewOrderEvent (un evento reset automatico) e l'ExitEvent (un evento di reset manuale).

Ma non sono sicuro se questo è thread-safe (presupponendo N thread di produzione che bloccano tutti la coda prima dell'eniviazione e un thread utente che esegue il codice sopra). Inoltre, possiamo supporre che la proprietà Queue.Count restituisca solo un valore Int32 dell'istanza dalla classe Queue (senza volatile o interbloccato o un blocco, ecc.).

Qual è il solito schema utilizzato con una coda e un AutoResetEvent per risolvere questo problema e fare ciò che sto cercando di fare con il codice sopra?

(Modificato per modificare leggermente la domanda dopo che è stato correttamente rilevato che Queue.Count poteva fare qualsiasi cosa e la sua implementazione specifica).

+0

Sicuramente questo ciclo è incorporato in un certo tipo di ciclo esterno (in esecuzione) - non è per questo che funziona bene? –

+0

No, nessun altro ciclo. C'è solo un thread che blocca la coda e annulla gli ordini e quindi imposta NewOrderEvent. –

risposta

4

sembra abbastanza thr Per me, WaitAny() si limiterà a completare immediatamente perché l'evento è già impostato. Questo è non un problema.

Non interrompere la sincronizzazione della filettatura che funziona. Ma se vuoi una trappola per topi migliore allora potresti considerare BlockingQueue di Joe Duffy in questo magazine article. Una versione più generale di esso è disponibile in .NET 4.0, System.Collections.Concurrent.BlockingCollection con ConcurrentQueue come implementazione pratica di esso.

+0

Ma poiché si tratta di un evento AutoResetEvent, se l'altro thread imposta l'evento prima che io abbia la possibilità di attendere, non avrò alcun problema. Sono d'accordo che se fosse un ManualResetEvent, allora è una storia diversa. –

+0

Whaaa ... è in 4.0? Come mai non ho ricevuto il memo? Ya impara qualcosa di nuovo ogni giorno. –

+0

@ Michael: questo non è il modo in cui ARE funziona, la chiamata Wait lo ripristina all'uscita. Non si resetta all'entrata, quindi attende. –

0

Si dovrebbe utilizzare solo il WaitAny per questo, e garantire che venga segnalato su ogni nuovo ordine aggiunto alla collezione PendingOrders:

while (WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 

      //Only if you want to exit when there are no more PendingOrders 
      return; 
     } 
    } 

    // Do Some Work if fbo is != null 
} 
+0

Se due elementi vengono messi in coda mentre uno è in esecuzione, solo Dequeue uno di essi, quindi attendi che un altro venga accodato. –

+0

Questo è esattamente il problema che mi ha portato ad usare il conteggio> 0 check in primo luogo. Se qualche altro thread accoda un elemento e imposta l'evento mentre sono nel ciclo while, mi mancherà. –

2

Utilizzando eventi manuali ...

ManualResetEvent[] CommandEventArr = new ManualResetEvent[] { NewOrderEvent, ExitEvent }; 

while ((WaitHandle.WaitAny(CommandEventArr) != 1)) 
{ 
    lock (PendingOrders) 
    { 
     if (PendingOrders.Count > 0) 
     { 
      fbo = PendingOrders.Dequeue(); 
     } 
     else 
     { 
      fbo = null; 
      NewOrderEvent.Reset(); 
     } 
    } 
} 

Poi è necessario garantire un blocco sul lato Enqueue così:

lock (PendingOrders) 
    { 
     PendingOrders.Enqueue(obj); 
     NewOrderEvent.Set(); 
    } 
+0

Sembra che funzioni. Lascia che ci pensi un po 'di più, ma mi sembra giusto. Grazie! –

+0

+1: ho solo dato un aspetto superficiale, ma credo che questa soluzione sia davvero sicura. La ragione per cui questo codice è più facile da capire è perché non prova nessuna di quelle fantasiose strategie lock-free (che sono ** enormi ** bandiere rosse). –

+0

Devo chiarire che questo è sicuro solo se esiste un singolo thread che esegue l'accodamento. Avere due o più thread in fase di accodamento può portare a una situazione in cui la coda ha un elemento ma l'ARE non è impostato. –

3

Sei corretto. Il codice non è thread-safe. Ma non per la ragione che pensi.

AutoResetEvent va bene. Tuttavia, solo perché acquisisci un blocco e riesegui il test di PendingOrders.Count. Il vero nodo della questione è che stai chiamando PendingOrders.Count al di fuori di un lucchetto. Poiché la classe Queue non è thread-safe, il tuo codice non è thread-safe ... period.

Ora in realtà probabilmente non avrai mai un problema con questo per due motivi. Innanzitutto, la proprietà Queue.Count è quasi certamente progettata per non lasciare mai l'oggetto in uno stato semi-cotto. Dopotutto, probabilmente restituirà solo una variabile di istanza. In secondo luogo, la mancanza di una barriera di memoria su quella lettura non avrà un impatto significativo nel più ampio contesto del tuo codice. La cosa peggiore che accadrà è che si otterrà una lettura stantio su un'iterazione del ciclo e quindi il blocco acquisito implicherà la creazione di una barriera di memoria e una nuova lettura avrà luogo alla successiva iterazione. Presumo qui che ci sia un solo elemento di accodamento del thread. Le cose cambiano considerevolmente se ci sono 2 o più.

Tuttavia, vorrei chiarirlo perfettamente. Non si ha garanzia che PendingOrders.Count non modificherà lo stato dell'oggetto durante la sua esecuzione. E poiché non è racchiuso in un blocco, un altro thread potrebbe iniziare un'operazione su di esso mentre è ancora in quello stato a metà schiena.

+0

Infatti, Queue.Count sembra solo restituire un campo int nell'implementazione corrente e non è contrassegnato come volatile. –

+0

Ciao Brian, per quanto riguarda il tuo primo punto, sono d'accordo. Un'implementazione arbitraria di Queue può fare qualsiasi cosa nella proprietà Count, incluso iterando attraverso tutti gli elementi. Dovrei cambiare la domanda per dire che possiamo supporre che Queue .Count restituisca appena una variabile di istanza (una lettura non volatile, non interbloccata). –

+0

Sono d'accordo che potrei ottenere una lettura stantio. Cioè, il valore reale di Count è 1, ma la mia lettura di Count nel ciclo while restituisce 0. Ma penso che sarebbe ancora ok. Il conteggio> 0 controllo è solo lì per cogliere il caso in cui i produttori accodano gli ordini M allo stesso tempo in modo che io non elabori solo uno degli ordini. Penso che tu abbia ragione - che la barriera di memoria della serratura mi salverà. Sto solo cercando di riflettere se nella pratica ci sono casi in cui ci sarebbe un problema. (Compreso di avere N thread di produzione, supponendo che ogni blocco blocchi la coda prima di accodare). –