EventHub ForEach并行异步

时间:2018-11-07 18:41:13

标签: c# multithreading async-await

总是设法使自己与异步工作混淆,我在这里经过一些验证/确认后,我正在做我认为在以下情况下正在做的事情。

给出以下简单示例:

// pretend / assume these are json msgs or something ;)
var strEvents = new List<string> { "event1", "event2", "event3" };

我可以简单地将每个事件发布到eventhub:

foreach (var e in strEvents)
{
    // Do some things
    outEventHub.Add(e); // ICollector
}

foreach将在单个线程上运行,并在内部依次执行每件事。.我猜到eventhub的发布也将保留在同一线程上?

将ICollector更改为IAsyncCollector,并实现以下目标:

foreach (var e in strEvents)
{
    // Do some things
    await outEventHub.AddAsync(e);
}

我想在这里说的是,foreach将在单个线程上运行,实际发送到事件中心的消息将在其他地方发送吗?或至少不阻塞同一线程。

更改为Parallel.ForEach事件,因为这些事件一次将到达100多个左右:

 Parallel.ForEach(events, async (e) =>
 {
      // Do some things
      await outEventHub.AddAsync(e);
 });

现在开始变得有些朦胧,因为我不确定真正现在正在发生什么... afaik每个事件都有它自己的线程(在硬件范围内)和步骤在该线程中不要阻塞它。.因此,除了这个琐碎的示例。

最后,我可以将它们全部转换为我认为的任务。

 private static async Task DoThingAsync(string e, IAsyncCollector<string> outEventHub)
 {
      await outEventHub.AddAsync(e);
 }

 var t = new List<Task>();

 foreach (var e in strEvents)
 {
      t.Add(DoThingAsync(e, outEventHub));
 }

 await Task.WhenAll(t);

现在我真的很朦胧,我认为这是在单个线程上进行准备。然后在任何可用线程上同时完全运行所有内容??

我很欣赏,为了确定哪种方法适合手头的基准测试是必需的...但是,现在解释一下框架在每种情况下的工作状况对我来说是非常有帮助的。

2 个答案:

答案 0 :(得分:2)

并行!=异步

这是这里的主要思想。它们都有自己的用途,可以一起使用,但是它们有很大的不同。您的假设基本上是正确的,但让我澄清一下:

简单的foreach

这是非并行非异步。没什么可谈的。

在foreach内部等待

这是非并行 async 代码。

foreach (var e in strEvents)
{
    // Do some things
    await outEventHub.AddAsync(e);
}

所有操作都将在单个线程上进行。它需要一个事件,开始将其添加到事件中心,并在此过程完成时(我猜它可以进行某种形式的网络交流) IO)将线程交还给线程池(如果在UI线程上被调用,则返回UI),以便它可以在等待AddAsync返回时做其他工作。但是正如您所说的,它根本不是并行的。

并行Foreach(异步)

这是一个陷阱!简而言之,Parallel.Foreach专为同步工作负载而设计。我们将回到这一点,但是首先让我们假设您将其与非异步代码一起使用。

并行foreach(同步)

又名并行但不异步。

Parallel.ForEach(events, (e) =>
 {
      // Do some things
      outEventHub.Add(e);
 });

每个项目都有自己的“任务”,但它们不会产生线程。创建线程是昂贵的,并且在最佳情况下,拥有比CPU内核更多的线程是没有意义的。相反,这些任务在 ThreadPool 上运行,该线程具有最佳线程数。每个线程执行一个任务,对其进行处理,然后执行另一个任务,依此类推。

您可以将其视为-在4核计算机上-围绕一堆任务有4个工人,因此一次要运行4个工人。您可以想象在受IO限制的工作负载的情况下这不是理想的(这很可能是)。如果您的网络速度很慢,则可以阻止所有4个线程将事件发送出去,而它们可能会做有用​​的工作。这导致我们...

任务

异步和可能并行(取决于用法)。

您的描述在这里也是正确的,除了ThreadPool之外,它一次完成了所有任务(在主线程上),然后在池的线程上运行。在它们运行时,释放主线程,然后可以根据需要执行其他工作。到目前为止,它与Parallel.Foreach情况相同。但是:

发生的事情是TaskPool线程拾取任务,进行必要的预处理,然后异步发出网络请求。这意味着该任务在等待网络时不会阻塞,而是释放 ThreadPool 线程来拾取另一个工作项。网络请求完成后,任务 continuation (网络请求之后的其余代码行)被调度回任务列表。

从理论上讲,您可以看到这是最有效的过程,它是如此之快,以至于您必须小心不要淹没网络。

返回Parallel.Foreach和异步

这时您应该可以发现问题。您所有的异步lambda async (e) => { await outEventHub.AddAsync(e);}所做的只是开始工作,它将在遇到await之后立即返回。 (请记住,异步/等待正在等待时释放线程。)Parallel.Foreach在启动所有线程之后立即返回。 但是没有什么在等待这些任务!这些变得一劳永逸,这通常是一种不好的做法。就像您从任务示例中删除了await Task.WhenAll调用一样。

我希望这能为您解决大部分问题,如果不能,请告诉我要改进的地方。

答案 1 :(得分:0)

为什么不并行地异步发送那些事件,像这样:

var tasks = new List<Task>();

foreach( var e in strEvents )
{
   tasks.Add(outEventHub.AddAsync(e));
}

await Task.WhenAll(tasks);
await outEventHub.FlushAsync();