处理多个异步调用

时间:2017-12-14 00:05:58

标签: c# asynchronous parallel-processing task

我有一个包含基本信息(如ID)的书籍列表,我需要调用多个外部API端点来获取其余信息,例如图片,参考文献,作者简历等。

这是一种性能不佳的方法,因为检索信息就像同步方式(当时一种方式)......

foreach (var book in books)
{
    book.Images = await GetImagesAsync(book.ID);
    book.Refs = await GetLinksReferencesAsync(book.ID);
    book.AuthorBio = await GetAuthorBioAsync(book.ID);
}

最终我想要的是获得async来电的优势并同时拨打100个电话。

一项改进是在Task.WaitAll()内添加foreach,但唯一的好处是当时要做3次请求。但是,如何以更有效的方式使用async调用来改善这一点,例如当时的100个请求?

2 个答案:

答案 0 :(得分:0)

我真的很想用TPL's dataflow library来做这件事。它允许您将多个异步操作(以及同步操作)链接在一起,形成一个“管道”。并且在每个阶段都有大量的调整来控制并行度,内存缓冲区大小等。

有各种类型的&#39;块&#39;你可以用它来组成一个管道。也许最简单的是TransformBlock<T1,T2>,它将函数映射为T1T2。还有ActionBlock<T>执行操作(但不返回值,因此实际上是管道的终止点)还有更多,例如:TransformManyBlock<T1,T2>BatchingBlock<T>,连接块,等更复杂的管道。

对于您自己的示例,您可以按如下方式设置管道。

首先我们定义一些默认选项(例如,我们指定最大缓冲区大小为100本书和15个同时作业的最大并行度):

var defaultOptions = new ExecutionDataflowBlockOptions
{
    BoundedCapacity = 100,
    MaxDegreeOfParallelism = 15
};

现在为前2个TransformBlock<Book,Book>作业定义GetXAsync,为每个作业定义一个ActionBlock,每次使用默认选项终止我们的管道:

var getImagesBlock = new TransformBlock<Book, Book>(async b =>
{
    b.Images = await GetImagesAsync(b.ID);
    return b;
}, defaultOptions);

var getLinksBlock = new TransformBlock<Book, Book>(async b =>
{
    b.Refs = await GetLinksReferencesAsync(b.ID);
    return b;
}, defaultOptions);

var getAuthorBioBlock = new ActionBlock<Book>(async b =>
{
    b.AuthorBio = await GetAuthorBioAsync(b.ID);
}, defaultOptions);

现在我们定义一些设置来管理我们如何链接块(探索这些最适合您自己的解决方案!):

var linkOptions = new DataflowLinkOptions
{
    PropagateCompletion = true //when an earlier block signals it is 'Complete' and has no more messages the next block completes too after it has finished any existing messages
};

然后我们将所有3个块连接起来构建管道:

getImagesBlock.LinkTo(getImagesBlock, linkOptions);
getLinksBlock.LinkTo(getAuthorBioBlock, linkOptions);

现在我们需要做的就是将每本书传递到管道的开头:

foreach (var book in books)
{
    getImagesBlock.Post(book); // or we could use SendAsync if this is inside an async method
}

向我们已完成发送图书的第一个块发出信号:

getImagesBlock.Complete();

然后等待最后一个块完成处理:

getAuthorBioBlock.Completion.Wait(); // or await getAuthorBioBlock.Completion; if inside an async method

我认为使用数据流选项,一旦习惯了它们,就可以为这样的大量并行操作提供一种自然,易用且广泛适用的解决方案。我建议花点时间学习如何使用它。它确实使这种类型的工作更容易管理和优化。

顺便提一下注意事项:

  • 我假设给定book的每个操作都必须按顺序执行,如OP中的情况。
  • 没有什么可以阻止我们在每个块使用不同的缓冲区大小或并行度。如果(通常情况下)不同的块具有不同类型或成本的操作,那么我们可以根据其自身的性能特征优化每个步骤。

答案 1 :(得分:0)

试试这个:

foreach (var book in books)
{
    var imagesTask = GetImagesAsync(book.ID);
    var refsTask = GetLinksReferencesAsync(book.ID);
    var authorTask = GetAuthorBioAsync(book.ID);

    Task.WaitAll(imagesTask, refsTask, authorTask);

    book.Images = imagesTask.Result;
    book.Refs = refsTask.Result;
    book.AuthorBio = authorTask.Result;
}

在这种方法中,三个异步任务同时执行。