c# - 使用 Dispacher.BeginInvoke 从并行循环调用 UI 线程
问题描述
我在下面有一个简单的异步代码:这是一个带有一个按钮和一个文本框的 WPF。我使用了一些包含五个整数的列表来模拟 5 个不同的任务。我的目的是实现当我并行和异步运行所有五个任务时,我可以观察到数字被一一添加到文本框中。而我做到了。方法“DoSomething”并行运行所有五个任务,每个任务都有不同的执行时间(由 Task.Delay 模拟),因此所有结果都会导致文本框中的数字一一出现。我无法弄清楚的唯一问题是:为什么在文本框中我首先显示字符串文本“这是结束文本”?!如果我等待方法 DoSomething 那么它应该首先完成,然后应该执行其余的代码。即使在我的情况下是重新绘制 GUI。
我猜这可能是由使用 Dispacher.BeginInvoke 引起的,这可能对异步/等待机制“造成一些干扰”。但我会很感激小线索以及如何避免这种行为。我知道我可以使用 Progress 事件来实现类似的效果,但是有没有其他方法可以使用 Parallel 循环并在 WPF 中逐步更新结果,避免我描述的这种意外行为?
private async void Button_Click(object sender, RoutedEventArgs e)
{
await DoSomething();
tbResults.Text += "This is end text";
}
private async Task DoSomething()
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
await Task.Run(()=> Parallel.ForEach(numbers,async i =>
{
await Task.Delay(i * 300);
await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
}));
}
// output is:
//This is end text 1 2 3 4 5 (all in separatę lines).
我的问题:
- 为什么在方法 DoSomething 之前显示文本。
- 如何解决它/避免它,解决它的任何替代方法(使用 Progress 事件除外)。任何信息将不胜感激。
解决方案
的线程Parallel.Foreach
是“真正的”后台线程。它们被创建并且应用程序继续执行。关键是这Parallel.Foreach
是不可等待的,因此在Parallel.Foreach
使用 挂起的线程时继续执行await
。
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.
}
解决方案是实现 Clemens 的评论建议的模式或使用 eg 的生产者消费者模式的异步实现,BlockingCollection
或者Channel
在分配“无限”数量的作业时获得对固定数量的线程的更多控制。
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;
}
讨论评论
“我的意图是并行运行任务并在完成后“报告”,即最短的任务将首先“报告”,依此类推。”
这正是上述解决方案中发生的事情。具有Task
最短延迟的将文本附加到第TextBox
一个。
“但是实施你建议的
await Task.WhenAll(tasks)
原因,我们需要等待所有任务完成,然后一次性报告。”
要按Task
完成顺序处理对象,您将替换Task.WhenAll
为Task.WhenAny
. 如果您不仅对第一个 completed 感兴趣,则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
但是由于您在这些线程内部调用,因此您以异步/等待方式挂起它们。
由于Parallel.Foreach
不可等待,它无法处理await
调用,并且就像挂起的线程自然完成一样。Parallel.Foreach
不明白线程只是被暂停await
并将在稍后继续。换句话说,等待链被破坏,因为Parallel.Foreach
无法将等待的上下文返回Task
到父等待Task.Run
上下文以表示其暂停。
这就是我说的线程Parallel.Foreach
不附加到Task.Run
. 它们与async/await基础设施完全隔离运行。
“异步 lambda 应该是“仅用于事件””
不,这是不正确的。当您将异步 lambda 传递给void
委托时,Action<T>
您是正确的:在这种情况下无法等待异步 lambda。但是,当将异步 lambda 传递给type的Func<T>
委托时,可以等待您的 lamda:T
Task
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
接受 a Func<Task>
,因此以下调用非常好:
Task.Run(async () => await Task.Delay(1));
“我猜你承认 .Net 6.0 带来了最好的解决方案:Parallel.ForEachASYNC![...] 我们可以产生几个线程并行处理我们的任务,我们可以等待整个循环,我们做到了不需要等待所有任务完成——他们在完成时“报告”“
那是错误的。支持使用async/awaitParallel.ForeachAsync
的线程,这是真的。实际上,您的原始示例将不再破坏预期的流程:因为在其线程中支持,它可以处理挂起的线程并将对象从其线程正确传播到调用者上下文,例如,到 wrapping 。
它现在知道如何等待和恢复挂起的线程。
重要提示:在其所有线程完成后仍会完成。您假设“他们在完成时“报告””是错误的。这是最直观的并发实现。枚举完所有项目后也完成。Parallel.ForeachAsync
await
Task
await Task.Run
Parallel.ForeachAsync
foreach
foreach
在对象完成时处理Task
对象的解决方案仍然使用Task.WhenAny
上面的模式。
一般来说,如果您不需要 and 的分区等额外功能Parallel.Foreach
,Parallel.ForeachAsync
您可以随时使用Task.WhenAll
。Task.WhenAll
尤其Parallel.ForeachAsync
是等效的,除了Parallel.ForeachAsync
默认提供更大的自定义:它支持节流和分区等技术,而无需额外的代码。
推荐阅读
- javascript - ASP.NET MVC Home\Index 页面中未使用 mmenu 插件
- angular - Angular:如果一个延迟加载的模块从另一个延迟加载的模块调用服务会发生什么?
- python - Selenium 不保持缓存有效
- python - 我正在观看 Mike Dane 关于 Guessing Game 的 Python 教程,我对代码感到困惑
- sql - Presto SQL 使用 ST_intersects 左连接,ST_crosses 产生意外结果
- html - 为什么gmail呈现outlook html
- daml - 如何找出 daml 中的因果单调性不一致错误?
- database - 如何关闭远程进度(openedge)数据库
- python - dataframe 有一个 url 的列表,漂亮的汤在变成字符串时会中断吗?
- api - 我可以在 php 中使用 codebird 获得某个推文的回复吗?