【问题标题】:How to make Parallel wait until all async task finished? [duplicate]如何让 Parallel 等到所有异步任务完成? [复制]
【发布时间】:2019-01-29 22:43:53
【问题描述】:

我正在制作资源加载代码。但是,有一个问题。

var toDoList = new List<Action>();

toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH1);
  Console.WriteLine("Load image");
});
toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH2);
  Console.WriteLine("Load image");
});

// ...

toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH100);
  Console.WriteLine("Load image");
});

toDoList.Add(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH1);
  Console.WriteLine("Load audio");
});

toDoList.Add(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH2);
  Console.WriteLine("Load audio");
});

// ...

toDoList.Add(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH100);
  Console.WriteLine("Load audio");
});

Console.WriteLine("Parallel Load Start");
Parallel.ForEach(toDoList, toDo => {
  toDo();
});
Console.WriteLine("Parallel Load End");

我想让 Parallel 等到所有 LoadAsync 函数完成。但是,由于异步操作委托,我没有等待。这表明

并行加载开始
加载音频
加载音频
加载图片
加载音频
加载图片
...
加载音频
并联负载端
加载图片
加载图片
加载图片
...

没有同步加载图像的方法,因为它是 Win2D 的一部分。 (= WinRT API)
我明白为什么所有磁盘 I/O 任务都必须是异步方法。
但是,上面的代码不是在 UI 线程而是在自定义线程上运行的。所以,它不需要异步运行。
您可以认为 Parallel 由于 async 方法而无用。但是,如您所见,toDoValues 既有异步(Win2D),也有同步(音频)。有效加载音频文件需要并行。

如何让 Parallel 等到所有资源都加载完毕?

【问题讨论】:

  • 是的,你是从错误的角度来的,只是使用了任务,然后使用 WhenAllWaitAll 等待它们
  • 您的问题与 Parallel 无关。并行等待,直到所有的动作都被执行。您启动异步任务,不要等到它们完成。
  • 您的一些操作本身就是async,因为没有返回Task,您有“即发即弃”的操作。该操作将在任务开始时完成,而不是在完成时完成。此类操作不直接适合与同步操作一起安排:要么全部为Func&lt;..., Task&lt;T&gt;&gt;,要么全部同步。
  • 我明白为什么会这样。我知道 Parallel 正在等待所有任务。但是,任务是异步方法。异步方法一调用就返回。因此,Parallel 认识到所有任务都返回并完成了。所以,我使用了 SemaphoreSlim (init 0, max 1)。并在并行后等待,并在加载所有资源时唤醒。它运作良好。但是,代码似乎很脏。
  • 我发布这个问题是因为 Win2D LoadAsync 方法没有任何同步方法。我昨天尝试了 LoadAsync -> Wait -> Result。但是,它会抛出 ArgumentException。但是,当我刚才再次尝试时,它起作用了! ???于是,就解决了。感谢所有 cmets!

标签: c#


【解决方案1】:

有效加载音频文件需要并行。

好吧,Parallel 是一种进行并行操作的方法。但是您的代码目前正在混合 CPU 绑定操作和异步操作,并尝试使用 Parallel 组合它们,这用于 CPU 绑定操作。这就是为什么它不能正常工作的原因。

更好的方法是使用Task.Run 而不是Parallel 将CPU 绑定操作推送到线程池。然后您可以将它们处理为异步的(从 UI 线程的角度来看),并使用 Task.WhenAll 将它们与自然异步 I/O 操作结合起来。

使用您的示例代码,它看起来像这样:

var toDoList = new List<Func<Task>>();
toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH1);
  Console.WriteLine("Load image");
});
toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH2);
  Console.WriteLine("Load image");
});

// ...

toDoList.Add(async () =>
{
  await CanvasBitmap.LoadAsync(PATH100);
  Console.WriteLine("Load image");
});

toDoList.Add(() => Task.Run(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH1);
  Console.WriteLine("Load audio");
}));

toDoList.Add(() => Task.Run(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH2);
  Console.WriteLine("Load audio");
}));

// ...

toDoList.Add(() => Task.Run(() =>
{
  AudioSystem.Instance.Load(AUDIO_PATH100);
  Console.WriteLine("Load audio");
}));

Console.WriteLine("Concurrent Load Start");
var tasks = toDoList.Select(toDo => toDo()).ToList();
Console.WriteLine("Concurrent Load Started");
await Task.WhenAll(tasks);
Console.WriteLine("Concurrent Load End");

【讨论】:

  • 非常感谢!有用!但是,当我想取消这个任务时,我该如何取消任务呢?当我使用 Parallel 时,CancellationToken 很有用。但是,在图像加载异步方法中,没有空间插入 CancellationToken。
  • @P.ful:您可以将CancellationToken 传递给您的代表并使用ThrowIfCancellationRequested。您可能还想使用SemaphoreSlim 限制一次运行的任务数。
【解决方案2】:

您需要使用WhenAll

Console.WriteLine("Parallel Load Start");
Parallel.ForEach(toDoList, toDo => {
  toDo();
});

await Task.WhenAll(toDoList); // Next line will be executed when all tasks are completed,

Console.WriteLine("Parallel Load End");

【讨论】:

  • 谢谢!我会试试的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-15
  • 1970-01-01
  • 2021-01-30
  • 2016-02-03
  • 2020-02-22
  • 2016-03-23
  • 1970-01-01
相关资源
最近更新 更多