【问题标题】:Handling tasks in parallel并行处理任务
【发布时间】:2021-08-31 17:16:00
【问题描述】:

考虑一个返回带有一些值的任务的 API。

我想根据这些值并行更新 UI(当其中一个值准备好时,我想更新它而不等待第二个值,假设每个值的更新都是它自己的更新方法)。

public async Task MyFunc()
{
    Task<First> firstTask = MyAPI.GetFirstValue();
    Task<Second> secondTask = MyAPI.GetSecondValue();

    UpdateFirstValueUI(await firstTask)
    UpdateSecondValueUI(await secondTask)
}

代码示例将等待第一个值,更新 UI,等待第二个值并再次更新 UI。

该场景的最佳做法是什么?我想知道ContinueWith 是否是最佳实践,因为我主要在遗留代码中看到它(之前有async-await)。

用一个更好的例子编辑: 假设我们有该 API 的两个实现并且代码看起来像这样

public async Task MyFunc()
{
    Task<First> firstTask = null
    Task<Second> secondTask = null
    if (someCondition) 
    {
        firstTask = MyAPI1.GetFirstValue();
        secondTask = MyAPI1.GetSecondValue();
    }
    else 
    {
        firstTask = MyAPI2.GetFirstValue();
        secondTask = MyAPI2.GetSecondValue();
    }

    UpdateFirstValueUI(await firstTask)
    UpdateSecondValueUI(await secondTask)
}

现在如您所见,我不想在两个不同的分支中调用更新方法(假设我们在分支后为每个 API 拆分该方法) 所以寻找一种方法来仅更改更新调用,以便它们可以并行发生

【问题讨论】:

  • 您无法在任何操作系统中从另一个线程更新 UI - 无论是 Windows、Mac 还是 Linux。如果要显示后台线程的进度,可以使用 Progress 类。根据您使用的 UI 堆栈(WinForms?WPF?UWP?Xamarin?MAUI?),您可以更新 ViewModel 对象,任何绑定的 UI 元素都会自行更新
  • @Selvin 避免等待所有任务完成。
  • await Task.WhenAll(CallGetFirstValueAndUpdateUI(), CallGetSecondValueAndUpdateUI()); ... 其中CallGetFirstValueAndUpdateUI 以类似方式定义为async Task CallGetFirstValueAndUpdateUI() { Task&lt;First&gt; firstTask = MyAPI.GetFirstValue(); UpdateFirstValueUI(await firstTask);} ... 和CallGetSecondValueAndUpdateUI

标签: c# asynchronous async-await


【解决方案1】:

ContinueWith 是一种primitive 方法,在库代码中很少使用,通常应在应用程序代码中避免使用。在您的情况下使用ContinueWith主要 问题是它将在ThreadPool 上执行延续,这不是您想要的,因为您的意图是更新UI。并从 UI 线程 is a no no 以外的任何其他线程更新 UI。可以通过将ContinueWith 配置为合适的TaskScheduler 来解决这个¹ 问题,但是使用异步/等待组合来解决它要简单得多。我的建议是在项目的某个静态类中添加下面的Run 方法:

public static class UF // Useful Functions
{
    public static async Task Run(Func<Task> action) => await action();
}

此方法只是调用并等待提供的异步委托。您可以使用此方法将异步 API 调用与其 UI 更新延续结合起来,如下所示:

public async Task MyFunc()
{
    Task<First> task1;
    Task<Second> task2;
    if (someCondition)
    {
        task1 = MyAPI1.GetFirstValueAsync();
        task2 = MyAPI1.GetSecondValueAsync();
    }
    else
    {
        task1 = MyAPI2.GetFirstValueAsync();
        task2 = MyAPI2.GetSecondValueAsync();
    }
    Task compositeTask1 = UF.Run(async () => UpdateFirstValueUI(await task1));
    Task compositeTask2 = UF.Run(async () => UpdateSecondValueUI(await task2));

    await Task.WhenAll(compositeTask1, compositeTask2);
}

这将确保在每个异步操作完成后立即更新 UI。

附带说明,如果您怀疑MyAPI 异步方法可能包含阻塞代码,您可以使用Task.Run 方法将它们卸载到ThreadPool,如下所示:

task1 = Task.Run(() => MyAPI1.GetFirstValueAsync());

要详细了解为什么这是一个好主意,您可以查看this answer

内置的Task.Run 方法和上面介绍的自定义UF.Run 方法的区别在于Task.RunThreadPool 上调用异步委托,而UF.Run 在当前线。如果您对比Run 更好的名称有任何想法,请提出建议。 :-)

¹ ContinueWith 也带来了大量其他问题,例如在AggregateExceptions 中包装错误,这使得错误地吞下异常变得容易,使得难以传播IsCanceled 的状态先前的任务,使得泄漏即发即弃的任务变得微不足道,需要由异步委托等创建的Unwrap嵌套Task&lt;Task&gt;s。

【讨论】:

  • 建议您在应用程序代码中避免使用的另一种方法以及ContinueWithConfigureAwait(false)。这也主要用于库代码。
猜你喜欢
  • 2017-04-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-12
  • 1970-01-01
  • 2015-07-18
  • 1970-01-01
相关资源
最近更新 更多