【问题标题】:Calling UI Thread from Parallel Loop using Dispacher.BeginInvoke [duplicate]使用 Dispacher.BeginInvoke 从并行循环调用 UI 线程 [重复]
【发布时间】:2021-12-26 01:22:26
【问题描述】:

我在下面有一个简单的异步代码:这是一个带有一个按钮和一个文本框的 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).

我的问题:

  1. 为什么在 DoSomething 方法之前显示文本。
  2. 如何解决它/避免它,解决它的任何替代方法(使用 Progress 事件除外)。 任何信息将不胜感激。

【问题讨论】:

标签: c# wpf asynchronous parallel-processing


【解决方案1】:

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.
}

解决方案是使用例如BlockingCollectionChannel 来实现 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&lt;T&gt;)时,您是对的:在这种情况下无法等待异步 lambda。但是,当将异步 lambda 传递给 T 类型为 TaskFunc&lt;T&gt; 委托时,您的 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&lt;Task&gt; 类型的委托,您就可以开始了。
这就是向Parallel.ForeachAsync 添加异步支持的方式:API 支持Func&lt;ValueTask&gt; 类型委托。例如Task.Run 接受Func&lt;Task&gt;,因此以下调用非常好:

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.ForeachParallel.ForeachAsync 的分区等额外功能,您可以随时使用Task.WhenAllTask.WhenAll 尤其是 Parallel.ForeachAsync 是等价的,除了 Parallel.ForeachAsync 默认提供更大的自定义:它支持诸如节流和分区之类的技术,而无需额外的代码。

【讨论】:

  • 哇,我必须承认我错了。确实在使用 Task.WhenAll 时 - 具有最短延迟的任务首先将文本附加到 TextBox。您的评论让我意识到在学习异步内容方面还有一段路要走。您的评论非常有价值和描述性。关于“等待链”的部分以及对异步 lambda 的解释很棒。确实非常讨厌你。
  • @kalka79 回来承认错误的好举动。非常强壮 。我很高兴能帮上忙。
【解决方案2】:

来自 Clemens 的好链接见评论。回答您的问题:

  1. Parallel.ForEach 中,您为 numbera 的每个条目启动/触发一个异步任务,您无需等待。所以你只等待,Parallel.ForEach 确实完成了,并且在它的异步任务之前完成了。
  2. 您可以做什么,例如删除Parallel.ForEach 内部的async 并使用Dispatcher.Invoke 而不是Dispatcher.BeginInvokeThread.Sleep 是一种反模式;),因此根据您的任务可能采取另一种解决方案(编辑:BionicCode 有一个不错的解决方案) :
private async Task DoSomething()
{
   var numbers = new List<int>(Enumerable.Range(1, 5));
   await Task.Run(()=> Parallel.ForEach(numbers, i =>
   {
      Thread.Sleep(i * 300);
      Dispatcher.Invoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
   }));
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-17
    • 2013-08-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多