如何让并行等待所有异步任务完成?

3
我正在编写一个资源加载代码,但是遇到了问题。
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函数完成后再执行。但是,由于异步Action委托,我没有等待。结果显示为:
Parallel Load Start 加载音频 加载音频 加载图像 加载音频 加载图像 ... 加载音频 Parallel Load End 加载图像 加载图像 加载图像 ...
由于它是Win2D的一部分(= WinRT API),所以没有同步加载图像的方法。 我理解为什么所有磁盘I/O任务必须是异步方法。 但是,上面的代码运行在非UI线程而是自定义线程上。因此,它不需要异步运行。 你可能认为Parallel由于异步方法是无用的。但是,正如您所看到的,toDoValues既具有异步方式(Win2D)又具有同步方式(Audio)。并且对于有效地加载音频文件,需要使用Parallel。
如何使Parallel等待所有资源加载完成?

9
你从错误的角度来看待这个问题了,只需要使用任务并使用 WhenAllWaitAll 等待它们全部完成。 - TheGeneral
1
你的问题与Parallel无关。Parallel会等待所有操作执行完毕。你启动了异步任务,但没有等待它们完成。 - Access Denied
你的一些操作本身就是“异步”的,因为没有返回任何“Task”,所以你有“fire-and-forget”操作。当任务已经启动时,该操作将完成,而不是在任务完成时。这样的操作不适合直接与同步操作一起调度:要么全部使用Func<..., Task<T>>,要么全部使用同步操作。 - Richard
我明白为什么会发生这种情况。我知道Parallel在等待所有任务完成。但是,这些任务是异步方法。异步方法一旦被调用就会立即返回。因此,Parallel认为所有任务都已经返回并且它结束了。所以,我使用了一个SemaphoreSlim(初始值为0,最大值为1),在Parallel之后等待,并在所有资源加载完毕时唤醒。这很有效。但是,代码看起来有点混乱。 - P. ful
我发布这个问题是因为Win2D的LoadAsync方法没有任何同步方法。昨天我尝试了LoadAsync -> Wait -> Result。但是,它抛出了ArgumentException异常。但是,当我刚刚再次尝试时,它奇迹般地工作了!所以,问题已经解决了。感谢所有的评论! - P. ful
2个回答

3
并行处理是为了有效地加载音频文件所必需的。
好的,"Parallel"是一种执行并行操作的方法。但是您的代码目前正在混合CPU绑定操作和异步操作,并尝试使用"Parallel"将它们组合起来,而"Parallel"仅适用于CPU绑定操作。这就是为什么它不能很好地工作的原因。
更好的方法是将CPU绑定操作推送到线程池中,使用"Task.Run"而不是"Parallel"。然后,您可以将它们视为异步操作(从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
@P.ful:你可以将 CancellationToken 传递给你的委托,并使用 ThrowIfCancellationRequested。你可能还想使用 SemaphoreSlim 来限制同时运行的任务数量。 - Stephen Cleary

1
你需要使用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");

谢谢!我会尝试的。 - P. ful

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接