2009-10-22 8 views
38

Dire che ho questo semplice metodo:C#: Come posso rendere sicuro un thread IEnumerable <T>?

public IEnumerable<uint> GetNumbers() 
{ 
    uint n = 0; 
    while(n < 100) 
     yield return n++; 
} 

Come ti rendere questo thread-safe? E con questo intendo che avresti ottenuto quell'enumeratore una sola volta, e che più thread gestiscono tutti i numeri senza che nessuno ottenga duplicati.

Suppongo che un blocco debba essere utilizzato da qualche parte, ma dove deve essere quel blocco per un blocco iteratore per essere thread-safe? Che cosa, in generale, hai bisogno di ricordare se vuoi un thread safe IEnumerable<T>? O piuttosto credo che sarebbe un thread sicuro IEnumerator<T> ...?

+4

(Nel caso in cui non stai controllando i miei commenti) Ho appena parlato di questo: http://msmvps.com/blogs/jon_skeet/archive/2009/10/23/iterating-atomically.aspx –

+2

Oh , Grazie! Questo sito dovrebbe avere una sorta di notifica Facebook quando qualcuno commenta dopo il tuo commento o qualcosa del genere ... hehe. – Svish

risposta

38

C'è un problema inerente nel fare ciò, perché IEnumerator<T> ha sia MoveNext() e Current. Si vuole veramente una singola chiamata come ad esempio:

bool TryMoveNext(out T value) 

a quel punto si può atomicamente passaggio all'elemento successivo e ottenere un valore. L'implementazione e il fatto che sia ancora in grado di utilizzare yield potrebbe essere complicato ... Ci penserò comunque. Penso che avresti bisogno di avvolgere l'iteratore "non-thread-safe" in uno thread-safe che ha eseguito atomicamente MoveNext() e Current per implementare l'interfaccia mostrata sopra. Non so come si sarebbe poi avvolgere questa interfaccia di nuovo in IEnumerator<T> in modo che si potrebbe usare in foreach però ...

Se stai usando .NET 4.0, estensioni parallele possono essere in grado di aiutare tu - avresti bisogno di spiegare di più su cosa stai cercando di fare però.

Questo è un argomento interessante - io possa avere a blog su di esso ...

EDIT: ora ho blogged about it con due approcci.

+3

Si dovrebbe chiamare 'TryMoveNext' per abbinare gli oggetti simili nel framework. :) –

+0

@ 280Z28: buona idea. Verrà modificato :) –

+0

Un modo in cui posso pensare che ti consentirebbe di usarlo in foreach sarebbe avere una classe che implementa IEnumerable e internamente memorizza l'elemento corrente in un campo threadlocal, in questo modo, quando un thread ha chiamato con successo MoveNext, il suo valore viene memorizzato per poterlo utilizzare in seguito, separato da altri thread. –

1

Suppongo che sia necessario un Enumeratore di risparmi sul thread, quindi è probabilmente necessario implementarlo.

-2

si può solo restituire una sequenza completa di volta in volta, piuttosto che all'uso Resa:

return Enumerable.Range(0, 100).Cast<uint>().ToArray();

+0

Non vuole elaborare l'intero intervallo in ogni thread. – Guillaume

+0

E anche io non voglio dover mettere l'intera gamma in una matrice o qualcosa del genere. – Svish

0

Beh, non sono sicuro, ma forse con alcune ciocche del chiamante?

Progetto:

Monitor.Enter(syncRoot); 
foreach (var item in enumerable) 
{ 
    Monitor.Exit(syncRoot); 
    //Do something with item 
    Monitor.Enter(syncRoot); 
} 
Monitor.Exit(syncRoot); 
0

stavo pensando che non è possibile effettuare il thread-safe yield parola chiave, a meno che non si rendono dipende da una fonte già thread-safe di valori:

public interface IThreadSafeEnumerator<T> 
{ 
    void Reset(); 
    bool TryMoveNext(out T value); 
} 

public class ThreadSafeUIntEnumerator : IThreadSafeEnumerator<uint>, IEnumerable<uint> 
{ 
    readonly object sync = new object(); 

    uint n; 

    #region IThreadSafeEnumerator<uint> Members 
    public void Reset() 
    { 
     lock (sync) 
     { 
      n = 0; 
     } 
    } 

    public bool TryMoveNext(out uint value) 
    { 
     bool success = false; 

     lock (sync) 
     { 
      if (n < 100) 
      { 
       value = n++; 
       success = true; 
      } 
      else 
      { 
       value = uint.MaxValue; 
      } 
     } 

     return success; 
    } 
    #endregion 
    #region IEnumerable<uint> Members 
    public IEnumerator<uint> GetEnumerator() 
    { 
     //Reset(); // depends on what behaviour you want 
     uint value; 
     while (TryMoveNext(out value)) 
     { 
      yield return value; 
     } 
    } 
    #endregion 
    #region IEnumerable Members 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     //Reset(); // depends on what behaviour you want 
     uint value; 
     while (TryMoveNext(out value)) 
     { 
      yield return value; 
     } 
    } 
    #endregion 
} 

Dovrete decidere se ogni avvio tipico di un enumeratore deve resettare la sequenza, o se il codice client deve farlo.

1

Ho appena provato questo pezzo di codice:

static IEnumerable<int> getNums() 
{ 
    Console.WriteLine("IENUM - ENTER"); 

    for (int i = 0; i < 10; i++) 
    { 
     Console.WriteLine(i); 
     yield return i; 
    } 

    Console.WriteLine("IENUM - EXIT"); 
} 

static IEnumerable<int> getNums2() 
{ 
    try 
    { 
     Console.WriteLine("IENUM - ENTER"); 

     for (int i = 0; i < 10; i++) 
     { 
      Console.WriteLine(i); 
      yield return i; 
     } 
    } 
    finally 
    { 
     Console.WriteLine("IENUM - EXIT"); 
    } 
} 

getNums2() chiama sempre finally parte del codice. Se vuoi che il tuo IEnumerable sia thread-safe, aggiungi i blocchi di thread che vuoi al posto di writelines, wither ReaderWriterSlimLock, Semaphore, Monitor, ecc.

Problemi correlati