Parallel.Foreach 的线程是“真正的”后台线程。它们被创建并且应用程序继续执行。关键是Parallel.Foreach 是不可等待的,因此在使用await 挂起Parallel.Foreach 的线程时继续执行。
private async Task DoSomething()
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
// Create the threads of Parallel.Foreach
await Task.Run(() =>
{
// Create the threads of Parallel.Foreach and continue
Parallel.ForEach(numbers,async i =>
{
// await suspends the thread and forces to return.
// Because Parallel.ForEach is not awaitable,
// execution leaves the scope of the Parallel.Foreach to continue.
await Task.Delay(i * 300);
await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
});
// After the threads are created the internal await of the Parallel.Foreach suspends background threads and
// forces to the execution to return from the Parallel.Foreach.
// The Task.Run thread continues.
Dispatcher.InvokeAsync(() => tbResults.Text += "Text while Parallel.Foreach threads are suspended");
// Since the background threads of the Parallel.Foreach are not attached
// to the parent Task.Run, the Task.Run completes now and returns
// i.e. Task.run does not wait for child background threads to complete.
// ==> Leave Task.Run as there is no work.
});
// Leave DoSomething() and continue to execute the remaining code in the Button_Click().
// Parallel.Foreach threads is still suspended until the await chain, in this case Button_Click(), is completed.
}
解决方案是使用例如BlockingCollection 或Channel 来实现 Clemens 的评论建议的模式或生产者消费者模式的异步实现,以便在分发“无限”的同时获得对固定数量的线程的更多控制工作数量。
private async Task DoSomething(int number)
{
await Task.Delay(number * 300);
Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
List<Task> tasks = new List<Task>();
// Alternatively use LINQ Select
foreach (int number in numbers)
{
Task task = DoSomething(number);
tasks.Add(task);
}
await Task.WhenAll(tasks);
tbResults.Text += "This is end text" + Environment.NewLine;
}
讨论cmets
“我的意图是并行运行任务并在它们完成后“报告”
完成,即最短的任务将首先“报告”
等等。”
这正是上述解决方案中发生的情况。具有最短延迟的Task 首先将文本附加到TextBox。
“但是执行您建议的await Task.WhenAll(tasks) 会导致
我们需要等待所有任务完成,然后在
一次。”
要按完成顺序处理Task 对象,您可以将Task.WhenAll 替换为Task.WhenAny。如果您不仅对第一个完成的Task 感兴趣,您还必须以迭代方式使用Task.WhenAny,直到所有Task 实例都完成:
按完成顺序处理所有任务对象
private async Task DoSomething(int number)
{
await Task.Delay(number * 300);
Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
List<Task> tasks = new List<Task>();
// Alternatively use LINQ Select
foreach (int number in numbers)
{
Task task = DoSomething(number);
tasks.Add(task);
}
// Until all Tasks have completed
while (tasks.Any())
{
Task<int> nextCompletedTask = await Task.WhenAny(tasks);
// Remove the completed Task so that
// we can await the next uncompleted Task that completes first
tasks.Remove(nextCompletedTask);
// Get the result of the completed Task
int taskId = await nextCompletedTask;
tbResults.Text += $"Task {taskId} has completed." + Environment.NewLine;
}
tbResults.Text += "This is end text" + Environment.NewLine;
}
"Parallel.ForEach 是不可等待的,所以我想把它包起来
Task.Run 允许我等待它,但这是因为正如你所说“自从
Parallel.Foreach 的后台线程未附加到
父Task.Run""
不,这不是我所说的。重点是我回答的第三句话:“重点是Parallel.Foreach 是不可等待的,因此在Parallel.Foreach 的线程使用等待暂停时继续执行。 "。
这意味着:通常Parallel.Foreach 同步执行:当Parallel.Foreach 的所有线程都完成时,调用上下文继续执行。但是由于您在这些线程中调用了await,因此您以 async/await 方式挂起它们。
由于Parallel.Foreach 是不可等待的,它不能处理await 调用,就像挂起的线程自然完成一样。 Parallel.Foreach 不明白线程只是被await 挂起,稍后会继续。换句话说,由于 Parallel.Foreach 无法将 Task 返回给等待的父级 Task.Run 上下文以表示其暂停,因此等待链中断。
这就是我说Parallel.Foreach 的线程未附加到Task.Run 时的意思。它们与 async/await 基础架构完全隔离运行。
"async lambdas 应该是"只与事件一起使用""
不,这是不正确的。当您将异步 lambda 传递给 void 委托(如 Action<T>)时,您是对的:在这种情况下无法等待异步 lambda。但是,当将异步 lambda 传递给 T 类型为 Task 的 Func<T> 委托时,您的 lamda 可以等待:
private void NoAsyncDelegateSupportedMethod(Action lambda)
{
// Since Action does not return a Task (return type is always void),
// the async lambda can't be awaited
lambda.Invoke();
}
private async Task AsyncDelegateSupportedMethod(Func<Task> asyncLambda)
{
// Since Func returns a Task, the async lambda can be awaited
await asyncLambda.Invoke();
}
public voi DoSoemthing()
{
// Not a good idea as NoAsyncDelegateSupportedMethod can't handle async lamdas: it defines a void delegate
NoAsyncDelegateSupportedMethod(async () => await Task.Delay(1));
// A good idea as AsyncDelegateSupportedMethod can handle async lamdas: it defines a Func<Task> delegate
AsyncDelegateSupportedMethod(async () => await Task.Delay(1));
}
如您所见,您的陈述不正确。您必须始终检查被调用方法的签名及其重载。如果它接受 Func<Task> 类型的委托,您就可以开始了。
这就是向Parallel.ForeachAsync 添加异步支持的方式:API 支持Func<ValueTask> 类型委托。例如Task.Run 接受Func<Task>,因此以下调用非常好:
Task.Run(async () => await Task.Delay(1));
" 我猜你承认 .Net 6.0 带来了最好的解决方案:
这是Parallel.ForEachASYNC! [...] 我们可以产生几个线程
它并行处理我们的任务,我们可以等待整个循环
我们不需要等待所有任务完成——它们“报告”为
他们完成了“
这是错误的。 Parallel.ForeachAsync 支持使用 async/await 的线程,这是真的。实际上,您的原始示例将不再破坏预期的流程:因为Parallel.ForeachAsync 在其线程中支持await,它可以处理挂起的线程并将Task 对象从其线程正确传播到调用者上下文,例如,到包装await Task.Run.
它现在知道如何等待和恢复挂起的线程。
重要提示:Parallel.ForeachAsync still 完成 AFTER ALL 其线程已完成。您假设 “他们在完成时“报告”” 是错误的。这是foreach 最直观的并发实现。 foreach 也会在枚举完所有项目后完成。
在完成时处理Task 对象的解决方案仍然使用上面的Task.WhenAny 模式。
一般来说,如果您不需要Parallel.Foreach 和Parallel.ForeachAsync 的分区等额外功能,您可以随时使用Task.WhenAll。 Task.WhenAll 尤其是 Parallel.ForeachAsync 是等价的,除了 Parallel.ForeachAsync 默认提供更大的自定义:它支持诸如节流和分区之类的技术,而无需额外的代码。