我刚刚开始阅读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。
答案 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();
}