2009-10-22 7 views
38

Dire que j'ai cette méthode simple:C#: Comment rendre un thread IEnumerable <T> sécurisé?

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

Comment voulez-vous faire de ce fil en toute sécurité? Et par là je veux dire que vous auriez une fois cette énumération, et que plusieurs threads géreraient tous les nombres sans que personne ne reçoive de doublons.

Je suppose qu'un verrou doit être utilisé quelque part, mais où doit être ce verrou pour qu'un bloc d'itérateur soit thread-safe? De quoi, en général, devez-vous vous rappeler si vous voulez un fil de sécurité IEnumerable<T>? Ou plutôt je suppose que ce serait un fil de sécurité IEnumerator<T> ...?

+4

(Juste au cas où vous ne vérifiez pas mes commentaires) Je viens de bloguer à ce sujet: http://msmvps.com/blogs/jon_skeet/archive/2009/10/23/iterating-atomically.aspx –

+2

Oh , Merci! Ce site devrait avoir une sorte de notification Facebook lorsque quelqu'un commente après votre commentaire ou quelque chose comme ça ... hehe. – Svish

Répondre

38

Il y a un problème inhérent à cela, car IEnumerator<T> a à la fois MoveNext() et Current. Vous voulez vraiment un appel unique tel que:

bool TryMoveNext(out T value) 

à ce moment-là, vous pouvez atomiquement passer à l'élément suivant et obtenir une valeur. Implémenter cela et être capable d'utiliser yield pourrait être difficile ... J'y penserai quand même. Je pense que vous auriez besoin d'envelopper l'itérateur "non-threadsafe" dans un thread-sûr qui a atomiquement effectué MoveNext() et Current pour implémenter l'interface montrée ci-dessus. Je ne sais pas comment vous Enveloppez ensuite cette interface de nouveau dans IEnumerator<T> afin que vous puissiez l'utiliser dans foreach si ...

Si vous utilisez .NET 4.0, les extensions parallèles peuvent être en mesure d'aider vous - vous auriez besoin d'expliquer plus sur ce que vous essayez de faire si.

Ceci est un sujet intéressant - je dois blog à ce sujet ...

EDIT: J'ai maintenant blogged about it avec deux approches.

+3

Vous devriez appeler cela TryMoveNext pour faire correspondre des éléments similaires dans le framework. :) –

+0

@ 280Z28: Bonne idée. Will edit :) –

+0

Une façon dont je peux penser à cela vous permettrait de l'utiliser dans foreach serait d'avoir une classe qui implémente IEnumerable, et stocke en interne l'élément actuel dans un champ threadlocal, de cette façon, quand un thread a réussi à appeler MoveNext, sa valeur est stockée pour plus tard, séparée des autres threads. –

1

Je suppose que vous avez besoin d'un énumérateur de thread-save, donc vous devriez probablement implémenter celui-là.

-2

Vous pouvez tout simplement retourner une séquence complète chaque fois plutôt que d'utiliser le rendement:

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

+0

Il ne veut pas traiter la totalité de la plage dans chaque thread. – Guillaume

+0

Et je ne veux pas non plus avoir à mettre toute la gamme dans un tableau ou quelque chose comme ça. – Svish

0

Eh bien, je ne suis pas sûr, mais peut-être avec des serrures à l'appelant?

Projet:

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

Je pensais que vous ne pouvez pas faire le fil de sécurité mot-clé yield, à moins que vous la faire dépendre d'une source de valeurs déjà thread-safe:

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 
} 

Vous devrez décider si chaque initiation typique d'un recenseur doit réinitialiser la séquence, ou si le code client doit le faire.

1

Je viens de tester ce morceau de code:

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() appelle toujours la partie finally du code. Si vous voulez que votre IEnumerable soit thread-safe, ajoutez les verrous de threads que vous voulez à la place des writelines, et utilisez ReaderWriterSlimLock, Semaphore, Monitor, etc.

Questions connexes