Comment créer une pompe à message personnalisée?

Je fais un petit exercice où je dois créer quelque chose qui ressemble à une pompe à message. Ce que j’ai est une queue de travail à faire, et je veux que le travail soit entièrement fait sur un thread alors que n’importe quel thread peut append du travail à la queue.

Queue queue; 

Les filets utilisent une poignée d’attente pour indiquer à la pompe qu’il rest du travail à faire.

 WaitHandle signal; 

La pompe tourne en boucle tant qu’il rest du travail à faire, puis attend le redémarrage du signal.

 while(ApplicationIsRunning){ while(queue.HasWork){ DoWork(queue.NextWorkItem) } signal.Reset(); signal.WaitOne(); } 

Chaque autre thread peut append du travail à la queue et signaler le descripteur d’attente …

 public void AddWork(WorkToDo work){ queue.Add(work); signal.Set(); } 

Le problème est que, si le travail est ajouté assez rapidement, une condition peut se produire: du travail peut être laissé dans la queue, car entre la recherche de travail de la queue et la réinitialisation de WaitHandle, un autre thread peut append du travail à la queue.

Comment pourrais-je remédier à cette situation sans mettre un mutex coûteux autour du WaitHandle?

Vous pouvez utiliser BlockingCollection pour faciliter la mise en œuvre de la file d’ attente, car elle gérera la synchronisation pour vous:

 public class MessagePump { private BlockingCollection actions = new BlockingCollection(); public void Run() //you may want to ressortingct this so that only one caller from one thread is running messages { foreach (var action in actions.GetConsumingEnumerable()) action(); } public void AddWork(Action action) { actions.Add(action); } public void Stop() { actions.CompleteAdding(); } } 

Vous ne devriez pas avoir à faire un mutex complet, mais pouvez-vous mettre une déclaration de locking

 public void AddWork(WorkToDo work) { queue.Add(work); lock(lockingObject) { signal.Set(); } } 

utilisez ce que vous voulez comme object verrouillé, la plupart des gens diraient qu’utiliser le signal lui-même est une mauvaise idée.

En réponse au commentaire de @ 500 – Server Internal Error ci-dessous, vous pouvez simplement réinitialiser le signal avant de commencer le travail. Ce qui suit devrait protéger les choses:

 while(ApplicationIsRunning) { while(queue.HasWork) { WorkItem wi; lock(lockingObject) { wi = queue.NextWorkItem; if(!queue.HasWork) { signal.Reset(); } } DoWork(wi) } signal.WaitOne(); } 

De cette façon, si vous avez plus de travail, la queue interne continue. Sinon, il tombe dans le signal.WaitOne() , et nous ne réinitialisons que s’il n’y a plus de travail déjà en queue.

Le seul inconvénient est qu’il est possible de réinitialiser plusieurs fois d’affilée si du travail arrive pendant l’exécution de DoWork .

Vous pouvez utiliser la méthode WaitOne (TimeSpan) pour obtenir une boucle hybride signal / interrogation. En gros, spécifiez une période d’attente d’au plus 1 seconde. Cela entraînerait le blocage d’une tâche dans cette course pendant au plus une seconde (ou la durée d’interrogation spécifiée) ou jusqu’à ce qu’une autre tâche soit ajoutée à votre queue.