TPL DataFlow工作流程

时间:2015-08-18 11:39:37

标签: c# tpl-dataflow

我刚刚开始阅读TPL Dataflow,这对我来说真的很困惑。我读过很多关于这个主题的文章,但我无法轻易消化它。可能很难,也许我还没有开始掌握这个想法。

我之所以开始研究这个问题,是因为我想实现一个可以运行并行任务但按顺序运行的场景,并发现TPL Dataflow可以用作这个。

我正在练习TPL和TPL Dataflow,而且我的初学者水平很高,所以我需要专家的帮助,他们可以指导我找到正确的方向。在我写的测试方法中,我做了以下事情,

private void btnTPLDataFlow_Click(object sender, EventArgs e)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();

        txtOutput.Clear();

        ExecutionDataflowBlockOptions execOptions = new ExecutionDataflowBlockOptions();
        execOptions.MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded;

        ActionBlock<string> actionBlock = new ActionBlock<string>(async v =>
        {
            await Task.Delay(200);
            await Task.Factory.StartNew(

                () => txtOutput.Text += v + Environment.NewLine, 
                CancellationToken.None,
                TaskCreationOptions.None,
                scheduler
                );

        }, execOptions);

        for (int i = 1; i < 101; i++)
        {
            actionBlock.Post(i.ToString());
        }

        actionBlock.Complete();

        watch.Stop();
        lblTPLDataFlow.Text = Convert.ToString(watch.ElapsedMilliseconds / 1000);
    }

现在程序是并行的并且都是异步的(不冻结我的UI),但生成的输出不是有序的,而我已经读过TPL Dataflow默认保持元素的顺序。所以我的猜测是,然后我创建的任务是罪魁祸首,它不会以正确的顺序输出字符串。我是对的吗?

如果是这种情况,那么我该如何制作这个异步并按顺序进行?

我试图将代码分开,并尝试将代码分发到不同的方法,但我的尝试失败,因为只有字符串输出到文本框而没有其他任何事情发生。

 private async void btnTPLDataFlow_Click(object sender, EventArgs e)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();

        await TPLDataFlowOperation();

        watch.Stop();
        lblTPLDataFlow.Text = Convert.ToString(watch.ElapsedMilliseconds / 1000);
    }

    public async Task TPLDataFlowOperation()
    {
        var actionBlock = new ActionBlock<int>(async values => txtOutput.Text += await ProcessValues(values) + Environment.NewLine,
            new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded, TaskScheduler = scheduler });

        for (int i = 1; i < 101; i++)
        {
            actionBlock.Post(i);
        }

        actionBlock.Complete();
        await actionBlock.Completion;
    }

    private async Task<string> ProcessValues(int i)
    {
        await Task.Delay(200);
        return "Test " + i;
    }

我知道我写了一段不好的代码,但这是我第一次尝试使用TPL Dataflow。

1 个答案:

答案 0 :(得分:1)

  

如何按顺序进行异步操作?

这有点矛盾。您可以按顺序创建 start 并发任务,但是您无法保证它们按顺序运行或完成。

让我们检查您的代码,看看发生了什么。

首先,您已选择DataflowBlockOptions.Unbounded。这告诉TPL Dataflow它不应该限制它允许并发运行的任务数量。因此,您的每个任务都将按顺序开始或多或少地开始。

您的异步操作以await Task.Delay(200)开头。这将导致您的方法暂停,然后在 200毫秒后恢复。但是,此延迟确切,并且从一次调用到下一次调用都会有所不同。此外,延迟后恢复代码的机制可能需要花费不同的时间。由于实际延迟的这种随机变化,下一个要运行的代码现在按顺序 - 导致您看到的差异。

您可能会发现此示例很有趣。它是一个简化操作的控制台应用程序。

class Program
{
    static void Main(string[] args)
    {
        OutputNumbersWithDataflow();
        OutputNumbersWithParallelLinq();

        Console.ReadLine();
    }

    private static async Task HandleStringAsync(string s)
    {
        await Task.Delay(200);
        Console.WriteLine("Handled {0}.", s);
    }

    private static void OutputNumbersWithDataflow()
    {
        var block = new ActionBlock<string>(
            HandleStringAsync,
            new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });

        for (int i = 0; i < 20; i++)
        {
            block.Post(i.ToString());
        }

        block.Complete();

        block.Completion.Wait();
    }

    private static string HandleString(string s)
    {
        // Perform some computation on s...
        Thread.Sleep(200);

        return s;
    }

    private static void OutputNumbersWithParallelLinq()
    {
        var myNumbers = Enumerable.Range(0, 20).AsParallel()
                                               .AsOrdered()
                                               .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
                                               .WithMergeOptions(ParallelMergeOptions.NotBuffered);

        var processed = from i in myNumbers
                        select HandleString(i.ToString());

        foreach (var s in processed)
        {
            Console.WriteLine(s);
        }
    }
}

使用与您的方法非常相似的方法计算第一组数字 - 使用TPL Dataflow。这些数字是无序的。

OutputNumbersWithParallelLinq()输出的第二组数字根本不使用数据流。它依赖于.NET内置的 Parallel LINQ 功能。这会在后台线程上运行我的HandleString()方法,但将数据按顺序保存到最后

这里的限制是PLINQ不允许您提供异步方法。 (嗯,你可以,但它不会给你所期望的行为。)HandleString()是一种传统的同步方法;它只是在后台线程上执行。

这是一个更复杂的数据流示例,可以保留正确的顺序

private static void OutputNumbersWithDataflowTransformBlock()
{
    Random r = new Random();
    var transformBlock = new TransformBlock<string, string>(
        async s =>
        {
            // Make the delay extra random, just to be sure.
            await Task.Delay(160 + r.Next(80));
            return s;
        },
        new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded });

    // For a GUI application you should also set the
    // scheduler here to make sure the output happens
    // on the correct thread.
    var outputBlock = new ActionBlock<string>(
        s => Console.WriteLine("Handled {0}.", s),
        new ExecutionDataflowBlockOptions
            { 
                SingleProducerConstrained = true,
                MaxDegreeOfParallelism = 1
            });

    transformBlock.LinkTo(outputBlock, new DataflowLinkOptions { PropagateCompletion = true });

    for (int i = 0; i < 20; i++)
    {
        transformBlock.Post(i.ToString());
    }

    transformBlock.Complete();

    outputBlock.Completion.Wait();
}