【问题标题】:How does parallelization work on async/await?并行化如何在 async/await 上工作?
【发布时间】:2019-06-30 21:03:26
【问题描述】:

我有以下代码,我打算异步运行。我的目标是 GetPictureForEmployeeAsync() 根据需要并行调用多次。我想确保 CreatePicture 上的“等待”不会阻止我这样做。

    public Task<Picture[]> GetPictures(IDictionary<string, string> tags)
    {
        var query = documentRepository.GetRepositoryQuery();

        var employees = query.Where(doc => doc.Gender == tags["gender"]);

        return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));
    }

    private Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
    {
        var base64PictureTask = blobRepository.GetBase64PictureAsync(employee.ID.ToString());
        var documentTask = documentRepository.GetItemAsync(employee.ID.ToString());
        return CreatePicture(tags, base64PictureTask, documentTask);
    }

    private static async Task<Picture> CreatePicture(IDictionary<string, string> tags, Task<string> base64PictureTask, Task<Employee> documentTask)
    {
        var document = await documentTask;

        return new Picture
        {
            EmployeeID = document.ID,
            Data = await base64PictureTask,
            ID = document.ID.ToString(),
            Tags = tags,
        };
    }

如果我理解正确,Task.WhenAll() 不会受到CreatePicture() 内的两个等待任务的影响,因为GetPictureForEmployeeAsync() 没有等待。我是对的吗?如果没有,我应该如何重组代码以实现我想要的?

【问题讨论】:

  • "Task.WhenAll 不受影响" -- 以什么方式受到影响?从某种意义上说,传递给WhenAll() 的每个任务最终都会在每个等待的任务完成时完成,这肯定会受到影响。您的实现似乎是合理的(尽管有编译时错误); GetPictureForEmployee() 方法返回 CreatePicture() 返回的任务,您通过 Select() 将您的输入数据投影到这些任务中创建了许多此类任务,然后您异步等待所有此类任务完成。跨度>
  • 说了这么多,最终还不清楚你在问什么。并行化在正确完成时与 async/await 一起工作得很好,而在不正确完成时则很差。就像其他任何事情一样。 Async/await 本身并不是真正的并行化。它可以在这种情况下使用,但它确实是一个更通用的概念,可用于 any 异步完成工作,无论是涉及并发处理某些工作集合还是单个异步操作。
  • 这看起来像是 TPL DataFlow 或 RX 的案例
  • 我建议你遵循在任务返回方法名称后缀Async的约定。
  • 如果您需要限制并行执行而不使用特殊解决方案(TPL 数据流、反应式扩展),这两个链接可能会有所帮助:Approaches for throttling asynchronous methods in C#Implementing a simple ForEachAsync, part 2

标签: c# .net asynchronous asp.net-core async-await


【解决方案1】:

我想确保 CreatePicture 上的“等待”不会阻止我这样做。

没有。

如果我理解正确的话,Task.WhenAll() 不会受到 CreatePicture() 中两个等待的任务的影响,因为 GetPictureForEmployeeAsync() 没有等待。我说得对吗?

是和不是。 WhenAll 不受CreatePicture 中等待的任务的任何限制,但这与是否等待GetPictureForEmployeeAsync 无关。这两行代码在行为上是等价的:

return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));
return Task.WhenAll(employees.Select(async employee => await GetPictureForEmployeeAsync(employee, tags)));

我建议阅读我的async intro,以更好地了解asyncawait 如何处理任务。

另外,由于GetPictures 具有非平凡的逻辑(GetRepositoryQuery 并评估tags["gender"]),我将recommend using async and await 用于GetPictures,因此:

public async Task<Picture[]> GetPictures(IDictionary<string, string> tags)
{
  var query = documentRepository.GetRepositoryQuery();
  var employees = query.Where(doc => doc.Gender == tags["gender"]);
  var tasks = employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)).ToList();

  return await Task.WhenAll(tasks);
}

最后一点,如果您不传递“本应等待的任务”,您可能会发现您的代码更干净 - 相反,await 他们首先传递它们的结果值:

async Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
{
  var base64PictureTask = blobRepository.GetBase64PictureAsync(employee.ID.ToString());
  var documentTask = documentRepository.GetItemAsync(employee.ID.ToString());
  await Task.WhenAll(base64PictureTask, documentTask);
  return CreatePicture(tags, await base64PictureTask, await documentTask);
}

static Picture CreatePicture(IDictionary<string, string> tags, string base64Picture, Employee document)
{
  return new Picture
  {
    EmployeeID = document.ID,
    Data = base64Picture,
    ID = document.ID.ToString(),
    Tags = tags,
  };
}

【讨论】:

  • 感谢您的出色回答,我想再问一个问题。调用 CreatePicture() 时,您等待之前创建的任务以及在前一行中等待的任务。在这种情况下使用结果不安全吗?因为我们知道这两项任务都已完成。
  • @Zalomon:你可以。我更喜欢使用await,因为它更耐重构。如果方法发生变化,从而不再保证任务完成(观察到异常情况),则 await 行为正常。
【解决方案2】:

关于调用异步方法要记住的一点是,一旦在该方法中到达await 语句,控制立即返回到调用异步方法的代码-- 无论 await 语句恰好在方法中的哪个位置。使用“正常”方法,控制不会返回到调用该方法的代码,直到到达该方法的末尾。

因此,在您的情况下,您可以执行以下操作:

private async Task<Picture> GetPictureForEmployeeAsync(Employee employee, IDictionary<string, string> tags)
    {
        // As soon as we get here, control immediately goes back to the GetPictures
        //   method -- no need to store the task in a variable and await it within
        //   CreatePicture as you were doing
        var picture = await blobRepository.GetBase64PictureAsync(employee.ID.ToString());
        var document = await documentRepository.GetItemAsync(employee.ID.ToString());
        return CreatePicture(tags, picture, document);
    }

因为 GetPictureForEmployeeAsync 的第一行代码有一个await,所以控制会立即回到这一行...

return Task.WhenAll(employees.Select(employee => GetPictureForEmployeeAsync(employee, tags)));

...一旦它被调用。这将产生并行处理所有员工项目的效果(嗯,有点——分配给您的应用程序的线程数将受到限制)。

另外建议,如果此应用程序正在访问数据库或 Web 服务以获取图片或文档,则此代码可能会导致您出现可用连接不足的问题。如果是这种情况,请考虑使用System.Threading.Tasks.Parallel 并设置最大并行度,或者使用SemaphoreSlim 来控制同时使用的连接数。

【讨论】:

  • 请注意,您提出的替代方案显着改变了 OP 代码的语义,坦率地说是负面的。特别是,当原始代码同时执行图片和文档检索时,您的版本会序列化这些操作,这可能会导致性能不佳,并且从用户的角度来看会增加延迟。
  • 你忘记了GetPictureForEmployeeAsync签名中的async。另外关于分配的线程数量有限,我们不知道异步方法GetBase64PictureAsyncGetItemAsync 的内部工作原理。我的猜测可能是there is no thread
  • @Byron: “原始代码序列化了这两个操作” -- 没有。 awaits 不序列化操作,因为在到达第一个 await 之前第二个任务已经开始。在您的代码中,第二个任务甚至不会 start 直到第一个 await 完成。这是非常不同的。
  • 例如“一旦在该方法中到达 await 语句,控制立即返回到调用异步方法的代码”不,那不是真的。如果任务已经正常完成,则获取结果并且协程不会产生。如果任务已经异常完成,则没有yield;而是调用异常处理机制。只有当任务不完整时,当前的工作流才会让给它的调用者。
  • 例如“使用‘正常’方法,控制权不会返回到调用该方法的代码,直到到达该方法的末尾。”我明白你在说什么,但要准确。在普通方法中——即不是异步方法或迭代器块——当到达方法的结尾或到达返回语句的结尾时,控制正常返回给调用者,而当达到了抛出方法未捕获的异常的结尾,除非有 finally... 等等。
猜你喜欢
  • 2018-01-10
  • 2018-11-26
  • 2020-03-09
  • 2021-12-18
  • 2017-06-28
  • 2019-12-04
  • 1970-01-01
  • 2015-11-15
相关资源
最近更新 更多