web-dev-qa-db-ja.com

TPLデータフロー、すべてのソースデータブロックが完了した場合にのみ完了を保証

両方のトランスフォームブロックが完了したときにコードが完了するコードをどのように書き直すことができますか?完了とは、完了とマークされ、「アウトキュー」が空であることを意味すると思いましたか?

public Test()
    {
        broadCastBlock = new BroadcastBlock<int>(i =>
            {
                return i;
            });

        transformBlock1 = new TransformBlock<int, string>(i =>
            {
                Console.WriteLine("1 input count: " + transformBlock1.InputCount);
                Thread.Sleep(50);
                return ("1_" + i);
            });

        transformBlock2 = new TransformBlock<int, string>(i =>
            {
                Console.WriteLine("2 input count: " + transformBlock1.InputCount);
                Thread.Sleep(20);
                return ("2_" + i);
            });

        processorBlock = new ActionBlock<string>(i =>
            {
                Console.WriteLine(i);
            });

        //Linking
        broadCastBlock.LinkTo(transformBlock1, new DataflowLinkOptions { PropagateCompletion = true });
        broadCastBlock.LinkTo(transformBlock2, new DataflowLinkOptions { PropagateCompletion = true });
        transformBlock1.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
        transformBlock2.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
    }

    public void Start()
    {
        const int numElements = 100;

        for (int i = 1; i <= numElements; i++)
        {
            broadCastBlock.SendAsync(i);
        }

        //mark completion
        broadCastBlock.Complete();

        processorBlock.Completion.Wait();

        Console.WriteLine("Finished");
        Console.ReadLine();
    }
}

コードを編集し、各変換ブロックの入力バッファー数を追加しました。明らかに、100個のアイテムすべてが各変換ブロックにストリーミングされます。ただし、トランスフォームブロックの1つが終了するとすぐに、processorblockはそれ以上のアイテムを受け入れず、代わりに、不完全なトランスフォームブロックの入力バッファーが入力バッファーをフラッシュするだけです。

23
Matthias Wolf

問題はまさにcasperOneが彼の答えで言ったことです。最初の変換ブロックが完了すると、プロセッサブロックは「終了モード」になります。入力キュー内の残りのアイテムを処理しますが、新しいアイテムは受け入れません。

ただし、プロセッサブロックを2つに分割するよりも簡単な修正があります。PropagateCompletionを設定せず、代わりに、両方の変換ブロックが完了したときにプロセッサブロックの完了を手動で設定します。

Task.WhenAll(transformBlock1.Completion, transformBlock2.Completion)
    .ContinueWith(_ => processorBlock.Complete());
32
svick

ここでの問題は、ブロックをリンクするために PropagateCompletionメソッド を呼び出すたびに LinkToプロパティ を設定していることです。変換ブロックでの待機時間。

Completeメソッド のドキュメントから IDataflowBlockインターフェイス (私の強調):

IDataflowBlockへのシグナルは受け入れない、メッセージを生成しない、延期されたメッセージを消費しない

TransformBlock<TInput, TOutput> インスタンスのそれぞれで待機時間をずらすため、transformBlock2(20ミリ秒待機)はtransformBlock1(50ミリ秒待機)の前に終了します。 。 transformBlock2が最初に完了し、次にシグナルをprocessorBlockに送信します。これにより、「他に何も受け入れていません」と表示されます(transformBlock1はまだすべてのメッセージを生成していません)。

transformBlock1の前のtransformBlock1の処理は、絶対に保証されないことに注意してください。スレッドプール(デフォルトのスケジューラーを使用していると仮定)がタスクを異なる順序で処理することは可能です(ただし、20ミリ秒の項目が完了するとキューから作業を盗むため、おそらくそうではありません)。

パイプラインは次のようになります。

           broadcastBlock
          /              \
 transformBlock1   transformBlock2
          \              /
           processorBlock

これを回避するには、次のようなパイプラインが必要です。

           broadcastBlock
          /              \
 transformBlock1   transformBlock2
          |              |
 processorBlock1   processorBlock2

これは、次のように2つの別々の ActionBlock<TInput> インスタンスを作成するだけで実現されます。

// The action, can be a method, makes it easier to share.
Action<string> a = i => Console.WriteLine(i);

// Create the processor blocks.
processorBlock1 = new ActionBlock<string>(a);
processorBlock2 = new ActionBlock<string>(a);


// Linking
broadCastBlock.LinkTo(transformBlock1, 
    new DataflowLinkOptions { PropagateCompletion = true });
broadCastBlock.LinkTo(transformBlock2, 
    new DataflowLinkOptions { PropagateCompletion = true });
transformBlock1.LinkTo(processorBlock1, 
    new DataflowLinkOptions { PropagateCompletion = true });
transformBlock2.LinkTo(processorBlock2, 
    new DataflowLinkOptions { PropagateCompletion = true });

次に、1つだけではなく、両方のプロセッサブロックで待機する必要があります。

Task.WhenAll(processorBlock1.Completion, processorBlock2.Completion).Wait();

ここで非常に重要な注意事項; ActionBlock<TInput>を作成する場合、デフォルトでは、渡された MaxDegreeOfParallelism インスタンスの ExecutionDataflowBlockOptionsプロパティ がに設定されます。 1。

これは、Action<T>に渡す ActionBlock<TInput>デリゲート への呼び出しはスレッドセーフであり、一度に1つだけが実行されることを意味します。

これで、同じActionBlock<TInput>デリゲートを指すtwoAction<T>インスタンスがあるため、スレッドセーフは保証されません。

メソッドがスレッドセーフの場合は、何もする必要はありません(理由がないため、MaxDegreeOfParallelismプロパティを DataflowBlockOptions.Unbounded に設定できます)。封鎖する)。

notスレッドセーフであり、それを保証する必要がある場合は、 lockステートメント のような従来の同期プリミティブに頼る必要があります。 =。

この場合、そのようにします(ただし、 WriteLineメソッドConsoleクラス のように、明らかに必要ありません。スレッドセーフです):

// The lock.
var l = new object();

// The action, can be a method, makes it easier to share.
Action<string> a = i => {
    // Ensure one call at a time.
    lock (l) Console.WriteLine(i);
};

// And so on...
26
casperOne

Svickの答えに加えて、PropagateCompletionオプションで得られる動作と一貫性を保つために、前のブロックに障害が発生した場合に例外を転送する必要もあります。次のような拡張メソッドもそれを処理します。

public static void CompleteWhenAll(this IDataflowBlock target, params IDataflowBlock[] sources) {
    if (target == null) return;
    if (sources.Length == 0) { target.Complete(); return; }
    Task.Factory.ContinueWhenAll(
        sources.Select(b => b.Completion).ToArray(),
        tasks => {
            var exceptions = (from t in tasks where t.IsFaulted select t.Exception).ToList();
            if (exceptions.Count != 0) {
                target.Fault(new AggregateException(exceptions));
            } else {
                target.Complete();
            }
        }
    );
}
8
pkt

ブロックに3つ以上のソースがある場合に、PropagateCompletion = trueが混乱する理由については、他の回答も非常に明確です。

この問題の簡単な解決策を提供するために、よりスマートな完了ルールが組み込まれているこの種の問題を解決するオープンソースライブラリ DataflowEx を調べることをお勧めします。 (内部でTPL Dataflowリンクを使用しますが、複雑な完了伝播をサポートします。実装はWhenAllに似ていますが、動的リンクの追加も処理します。 Dataflow.RegisterDependency() および TaskEx.AwaitableWhenAll( ) 暗黙の詳細。)

DataflowExを使用してすべてが機能するように、コードを少し変更しました。

public CompletionDemo1()
{
    broadCaster = new BroadcastBlock<int>(
        i =>
            {
                return i;
            }).ToDataflow();

    transformBlock1 = new TransformBlock<int, string>(
        i =>
            {
                Console.WriteLine("1 input count: " + transformBlock1.InputCount);
                Thread.Sleep(50);
                return ("1_" + i);
            });

    transformBlock2 = new TransformBlock<int, string>(
        i =>
            {
                Console.WriteLine("2 input count: " + transformBlock2.InputCount);
                Thread.Sleep(20);
                return ("2_" + i);
            });

    processor = new ActionBlock<string>(
        i =>
            {
                Console.WriteLine(i);
            }).ToDataflow();

    /** rather than TPL linking
      broadCastBlock.LinkTo(transformBlock1, new DataflowLinkOptions { PropagateCompletion = true });
      broadCastBlock.LinkTo(transformBlock2, new DataflowLinkOptions { PropagateCompletion = true });
      transformBlock1.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
      transformBlock2.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
     **/

    //Use DataflowEx linking
    var transform1 = transformBlock1.ToDataflow();
    var transform2 = transformBlock2.ToDataflow();

    broadCaster.LinkTo(transform1);
    broadCaster.LinkTo(transform2);
    transform1.LinkTo(processor);
    transform2.LinkTo(processor);
}

完全なコードは ここ です。

免責事項:私はDataflowExの作成者であり、MITライセンスで公開されています。

1
Dodd