2016-12-21 1 views
0

A partir d'une boucle de jeu je veux commencer à travailler en arrière-plan qui devrait être exécuté l'un après l'autre mais ne devrait pas bloquer la boucle de jeu.Comment mettre en file d'attente des délégués à exécuter en série en arrière-plan avec C#?

Donc, idéalement une classe BackgroundQueue qui pourrait être utilisé comme ceci:

BackgroundQueue myQueue = new BackgroundQueue(); 
//game loop called 60 times per second 
void Update() 
{ 
    if (somethingHappens) 
    { 
     myQueue.Enqueue(() => MethodThatTakesLong(someArguments)) 
    } 
} 

Y at-il une classe prêt faite en .NET qui fonctionne pour le scénario? Ou quelqu'un sait-il un bon exemple sur la façon d'implémenter la classe BackgroundQueue?

Un agréable d'avoir serait si la classe pourrait indiquer si elle est en train de faire quelque chose et combien de délégués sont mis en attente jusqu'à ...

+0

Regardez BlockingCollection avec ConcurrentQueue, il existe de nombreux exemples. Commencez un thread qui utilise BlockingCollection et des éléments de votre boucle principale. – Evk

+2

La tâche et un TaskScheduler personnalisé feront probablement l'affaire si vous voulez que vos délégués soient exécutés séquentiellement. Voir http://www.infoworld.com/article/3063560/application-development/building-your-own-task-scheduler-in-c.html pour savoir comment créer un TaskScheduler personnalisé. Si cela ne vous dérange pas que vos délégués soient exécutés en parallèle, alors le .NET TaskScheduler standard est probablement très bien. –

+1

Envisagez de conserver tout sur un thread et d'utiliser async-await pour créer un flux de travail asynchrone. Rappelez-vous, ** asynchrony n'est pas la concurrence **. Vous pouvez faire des flux de travail asynchrones sur un seul fil, tout comme vous pouvez faire cuire des œufs et des toasts en même temps sans embaucher deux cuisiniers. –

Répondre

3

Une solution est de l'excellente Threading in C# E-book. Dans leur section sur les structures de base, l'auteur fait presque exactement ce que vous cherchez in an example.

Suivez ce lien et descendez jusqu'à la file d'attente Producer/Consumer. Dans une section ultérieure, il explique que, bien que ConcurrentQueue fonctionnerait bien aussi, il fonctionne dans tous les cas, sauf dans des scénarios hautement concurrents. Mais pour votre cas de charge faible, il peut être préférable de faire fonctionner quelque chose facilement. Je n'ai pas d'expérience personnelle avec la revendication du document, mais vous pouvez l'évaluer.

J'espère que cela aide. Edit2: à la suggestion d'Evk (merci!), La classe BlockingCollection ressemble à ce que vous voulez. Par défaut, il utilise une ConcurrentQueue sous le capot. J'aime particulièrement la méthode CompleteAdding, ainsi que la possibilité d'utiliser CancellationTokens avec elle. Les scénarios "Shutdown" ne sont pas toujours correctement pris en compte lorsque les choses sont bloquantes, mais cela ne fonctionne pas correctement.

Édition 3: Comme demandé, un exemple de comment cela fonctionnerait avec un BlockingCollection. J'ai utilisé le foreach et GetConsumingEnumerable pour rendre cela encore plus compact pour le côté consommateur du problème:

using System.Collections.Concurrent; 
private static void testMethod() 
{ 
    BlockingCollection<Action> myActionQueue = new BlockingCollection<Action>(); 
    var consumer = Task.Run(() => 
    { 
    foreach(var item in myActionQueue.GetConsumingEnumerable()) 
    { 
     item(); // Run the task 
    }// Exits when the BlockingCollection is marked for no more actions 
    }); 

    // Add some tasks 
    for(int i = 0; i < 10; ++i) 
    { 
    int captured = i; // Imporant to copy this value or else 
    myActionQueue.Add(() => 
    { 
     Console.WriteLine("Action number " + captured + " executing."); 
     Thread.Sleep(100); // Busy work 
     Console.WriteLine("Completed."); 
    }); 
    Console.WriteLine("Added job number " + i); 
    Thread.Sleep(50); 
    } 
    myActionQueue.CompleteAdding(); 
    Console.WriteLine("Completed adding tasks. Waiting for consumer completion"); 

    consumer.Wait(); // Waits for consumer to finish 
    Console.WriteLine("All actions completed."); 
} 

I ajouté dans le sommeil() appels de sorte que vous pouvez voir que les choses sont ajoutés alors que d'autres choses sont consommées . Vous pouvez également choisir de lancer n'importe quel nombre de consumer lambda (appelez-le Action, puis lancez le Action plusieurs fois) ou la boucle d'addition. Et à tout moment vous pouvez appeler Count sur la collection pour obtenir le nombre de tâches qui ne sont pas en cours d'exécution. Vraisemblablement, si ce n'est pas zéro, alors les tâches de votre producteur sont en cours d'exécution.

+2

Si vous utilisez BlockingCollection avec ConcurrentQueue (et sa méthode GetConsumingEnumerable) - il va bloquer lors de la suppression. – Evk

1

Que diriez-vous de cette

void Main() 
{ 
    var executor = new MyExecutor(); 
    executor.Execute(()=>Console.WriteLine("Hello")); 
    executor.Execute(()=>Console.WriteLine(",")); 
    executor.Execute(()=>Console.WriteLine("World")); 
} 

public class MyExecutor 
{ 
    private Task _current = Task.FromResult(0); 

    public void Execute(Action action) 
    { 
     _current=_current.ContinueWith(prev=>action()); 
    } 
} 

UPD

de code mis à jour. Maintenant, nous sommes en mesure d'obtenir le nombre d'actions, pousser à partir de différents threads, etc

void Main() 
{ 
    var executor = new MyExecutor(); 
    executor.Execute(() => Console.WriteLine("Hello")); 
    executor.Execute(() => Thread.Sleep(100)); 
    executor.Execute(() => Console.WriteLine(",")); 
    executor.Execute(() => { throw new Exception(); }); 
    executor.Execute(() => Console.WriteLine("World")); 
    executor.Execute(() => Thread.Sleep(100)); 

    executor.WaitCurrent(); 

    Console.WriteLine($"{nameof(MyExecutor.Total)}:{executor.Total}"); 
    Console.WriteLine($"{nameof(MyExecutor.Finished)}:{executor.Finished}"); 
    Console.WriteLine($"{nameof(MyExecutor.Failed)}:{executor.Failed}"); 
} 

public class MyExecutor 
{ 
    private Task _current = Task.FromResult(0); 
    private int _failed = 0; 
    private int _finished = 0; 
    private int _total = 0; 
    private object _locker = new object(); 

    public void WaitCurrent() 
    { 
     _current.Wait();   
    } 

    public int Total 
    { 
     get { return _total; } 
    } 

    public int Finished 
    { 
     get { return _finished; } 
    } 

    public int Failed 
    { 
     get { return _failed; } 
    } 

    public void Execute(Action action) 
    { 
     lock (_locker) // not sure that lock it is the best way here 
     { 
      _total++; 
      _current = _current.ContinueWith(prev => SafeExecute(action)); 
     } 
    } 

    private void SafeExecute(Action action) 
    { 
     try 
     {    
      action(); 
     } 
     catch 
     { 
      Interlocked.Increment(ref _failed); 
     } 
     finally 
     { 
      Interlocked.Increment(ref _finished); 
     } 
    } 
} 
+0

Cela semble fonctionner, tant que vous n'ajoutez que des tâches à un thread. Dans le cas de l'OP, c'est bien, mais je ne pense pas que cela fonctionnerait à partir de plusieurs threads "producteurs", et vous n'obtiendrez pas le souhait du PO de voir combien de tâches sont en file d'attente. Editer: aussi, que se passerait-il si les tâches sont terminées, est-ce que ContinueWith 'Task' nouvellement ajouté s'exécutera immédiatement? –

+0

@KevinAnderson mise à jour de la réponse. Si la tâche est terminée, la continuation commencera immédiatement. – tym32167

+0

Des ajouts intéressants, mais votre variable '_total' ne se mettra pas à jour tant que la tâche ne sera pas lancée, donc je ne pense pas que cela lui donnera le nombre de' Task's qu'il a mis en file d'attente. Fondamentalement, après tout cet effort, je pense que la suggestion de @Evk d'un 'BlockingCollection' de' Action's ou 'Task's est plus propre. Mais d'autres peuvent être en désaccord. –