2016-12-21 1 views
0

ゲームループから、私はバックグラウンドで次々に実行すべき作業を開始したいが、ゲームループをブロックすべきではない。デリゲートをC#でバックグラウンドで直列に実行する方法をキューに入れる方法は?

だから、理想的には、このように使用することができ、クラスBackgroundQueue

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

シナリオのために働く.NETの既製のクラスがありますか?または、誰かがBackgroundQueueクラスの実装方法の良い例を知っていますか?クラスは、それが現在何かをやっているとどのように多くの代表者がキューに登録されているかどうかを報告することができれば

ていていいのだろう...

+0

ConcurrentQueueと一緒にBlockingCollectionを見ると、多くの例があります。 BlockingCollectionから消費されるスレッドを開始し、メインループからそのスレッドへのアイテムを開始します。 – Evk

+2

タスクとカスタムTaskSchedulerは、デリゲートを順番に実行する場合はおそらくトリックを行います。カスタムTaskSchedulerを構築する方法については、http://www.infoworld.com/article/3063560/application-development/building-your-own-task-scheduler-in-c.htmlを参照してください。あなたの代理人が並行して実行されても構わないのであれば、標準の.NET TaskSchedulerはおそらく大丈夫です。 –

+1

すべてを1つのスレッドに保持し、async-awaitを使用して非同期ワークフローを構築することを検討してください。 **非同期は並行性**ではないことを覚えておいてください。 2つのCookを雇うことなく、同時に卵とトーストを調理できるように、非同期ワークフローを1つのスレッドですべて実行できます。 –

答えて

3

一つの解決策は、優れたThreading in C# E-bookからです。基本的な構造に関するセクションでは、著者はあなたが探しているものをほぼ正確に作成しますin an example

このリンクをクリックして、プロデューサ/コンシューマキューにスクロールします。

以降のセクションでは、ConcurrentQueueも正常に機能すると指摘していますが、並行性の高いシナリオを除いてすべて悪化します。しかし、あなたの低負荷のケースでは、簡単に何かを手に入れるほうが良いかもしれません。私は文書からの主張に個人的な経験はありませんが、あなたが評価するためにそこにあります。

こちらがお役に立てば幸いです。

Edit2:Evkの提案(ありがとう!)に応じて、BlockingCollectionクラスはあなたの望むように見えます。デフォルトでは、フードの下でConcurrentQueueを使用します。私は特にCompleteAddingメソッドが好きで、それとCancellationTokensを使うことができます。状況が「ブロッキング」しているときに「シャットダウン」シナリオが正しく説明されているとは限りませんが、これは正しいIMOです。

編集3:要求に応じて、これがBlockingCollectionでどのように機能するかのサンプル。 )(私はスリープに追加

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."); 
} 

他のものが消費されている間、あなたは物事が追加されていることを見ることができるように呼び出します:私は、問題の消費者側のために、これは、よりコンパクトにするforeachGetConsumingEnumerableを使用しました。また、任意の数のconsumerラムダを起動することもできます(Actionと呼び、Actionを複数回起動します)。いつでも、コレクション上でCountに電話して、実行されていないタスクの数を取得することができます。おそらくゼロでない場合、プロデューサのタスクが実行されていると考えられます。

+2

ConcurrentQueue(とそのメソッドGetConsumingEnumerable)でBlockingCollectionを使用すると、削除中にブロックされます。 – Evk

1

どのようにこの

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

更新されたコードについて。今度はアクションの数を増やしたり、異なるスレッドからプッシュしたりすることができます。

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

これは、あなたがたった一つのスレッドから 'Tasks'を追加する限り、うまくいくようです。 OPのケースでは問題ありませんが、複数の "プロデューサ"スレッドからはうまくいかないと思っています。また、キューに入れられているジョブの数を確認するOPの希望はありません。編集:また、タスクが完了すると、何が起こるのだろう、新しく追加されたContinueWith 'タスクはすぐに実行されますか? –

+0

@KevinAndersonが答えを更新しました。タスクが終了すると、継続がすぐに開始されます。 – tym32167

+0

興味深い追加ですが、あなたの '_total'変数はタスクの実行が始まるまで更新されないので、彼がキューに入れた' Task'sの数を彼に与えるとは思いません。基本的には、その努力の後、@Evkからの 'Action'や' Task'sの 'BlockingCollection'の提案はよりクリーンであると私は思っています。しかし、他の人は反対するかもしれない。 –

関連する問題