等待和手动任务处理之间的区别

Difference between await and manual Task handling

我希望有人澄清等待许多任务和手动等待它们在集合中完成之间的区别。

我有一个 XML reader class 必须处理数百个文件,在优化它时我发现了一个有趣的区别。我创建了两个函数,都调用了我的 private async Task ParseModelsFromFile(ConcurrentDictionary<TSettings, TResults> datas, string filePath) 函数。这基本上从给定 filePath 上的 XML 文件加载我的所有模型。唯一的区别在于此 ParseModelsFromFile() 函数的调用和等待。

第一个函数:ProcessFiles() 使用 await 关键字,第二个函数:ProcessFilesWithTasks 为每个 filePath 创建一个 Task 对象,将其放入在 Task 的集合中并等待它们全部完成。

带有任务集合的第二个版本要快得多。大约需要一半的时间。 我在想这两个函数会做同样的事情,唯一的区别是 async 关键字的使用。

我确实用几个文件对此进行了测试,使用 await 的函数总是慢 2 倍左右。

    private async Task ProcessFiles(ConcurrentDictionary<TSettings, TResults> datas, BlockingCollection<string> filePaths)
    {
        if (filePaths.Count == 0)
            return;

        foreach (var filePath in filePaths.GetConsumingEnumerable())
        {
            await ParseModelsFromFile(datas, filePath);
        }
    }

    private Task ProcessFilesWithTasks(ConcurrentDictionary<TSettings, TResults> datas, BlockingCollection<string> filePaths)
    {
        if (filePaths.Count == 0)
            return Task.CompletedTask;

        List<Task> runningTasks = new List<Task>();

        foreach (var filePath in filePaths.GetConsumingEnumerable())
        {
            runningTasks.Add(ParseModelsFromFile(datas, filePath));
        }

        Task allTasks = Task.WhenAll(runningTasks.ToArray());
        allTasks.Wait();

        return Task.CompletedTask;
    }

如果您了解异步代码的工作原理,您就会明白其中的原因。那么让我们先看看 ParseModelsFromFile 做了什么。当它被调用时,它 运行s 直到该方法中的第一个 await ,这可能是它读取文件的地方。在 OS 和硬件读取文件期间,您的应用程序无事可做。正是在这一点上,ParseModelsFromFilereturns。它 returns 一个 Task,您可以使用它来了解方法的其余部分何时完成。

知道了,我们可以谈谈你的两个实现之间的区别:

使用await意味着:在此任务完成之前不要做任何事情。线程被释放出来做其他事情,这就是异步编程的好处。但是您的方法的执行会停止,直到任务完成。因此在 foreach 循环中使用 await 将处理一个文件并等待它完全处理后再进入循环的下一次迭代。

而在 ProcessFilesWithTasks 中,因为您在调用 ParseModelsFromFile 时没有使用 await,所以您使用的是 OS 检索文件的时间移动到循环的下一次迭代并为下一个文件开始 运行ning ParseModelsFromFile

如果这是在没有同步上下文的应用程序中(例如 ASP.NET 核心、控制台或 Windows 服务),则每次调用 ParseModelsFromFile(第一个 await 之后的所有内容)将 运行 在不同的线程上,这也将使每个文件的处理速度更快。

我唯一要做的改变是 ProcessFilesWithTasks async 并在 Task.WhenAll() 上使用 await 而不是 .Wait():

private async Task ProcessFilesWithTasks(ConcurrentDictionary<TSettings, TResults> datas, BlockingCollection<string> filePaths)
{
    if (filePaths.Count == 0)
        return Task.CompletedTask;

    List<Task> runningTasks = new List<Task>();

    foreach (var filePath in filePaths.GetConsumingEnumerable())
    {
        runningTasks.Add(ParseModelsFromFile(datas, filePath));
    }

    await Task.WhenAll(runningTasks);
}

Wait() 将阻止线程执行任何其他操作。它只会闲置。使用 await 将释放线程在等待时做其他事情。

您也不需要调用 .ToArray(),因为 Task.WhenAll() 接受 IEnumerable<Task>,而 List<Task> 是。

Microsoft 在 Asynchronous programming with async and await 上有一些真正 well-written 的文章,我认为您会从阅读中受益。