C# Parallel.ForEach 和 Task.WhenAll 有时返回的值比假设的要少

Mar*_*lPT 3 c# task async-await parallel-foreach configureawait

我有这个:

Parallel.ForEach(numbers, (number) =>
{
    var value = Regex.Replace(number, @"\s+", "%20");

    tasks.Add(client.GetAsync(url + value));
});

await Task.WhenAll(tasks).ConfigureAwait(false);

foreach (var task in tasks)
{
  ...
}
Run Code Online (Sandbox Code Playgroud)

有时在到达 foreach(任务中的 var 任务)时返回较少的任务,但在几次请求后,开始返回所有任务。

我已将 ConfigureAwait 更改为 true,但有时仍会返回较少的任务。

顺便说一句,我使用 Parallel.ForEach,因为每个 client.GetAsync(url + value) 都是对外部 api 的请求,其特殊性在于其 99% 的请求的延迟 SLA 低于 1s

你们能解释一下为什么它有时会返回较少的任务吗?

有没有办法保证总是返回所有任务?

谢谢

Cai*_*ard 8

有没有办法保证总是返回所有任务?

评论中的几个人指出你应该这样做,假设它numbers是一个非线程安全列表:

    foreach(var number in numbers)
    {
        var value = Regex.Replace(number, @"\s+", "%20");

        tasks.Add(client.GetAsync(url + value));
    }

    await Task.WhenAll(tasks).ConfigureAwait(false);

    foreach (var task in tasks)
    {
      ...
    }
Run Code Online (Sandbox Code Playgroud)

并行创建下载任务似乎没有任何显着的好处;这发生得非常快。等待下载完成是在WhenAll

ps; 有多种更复杂的方法可以为 URL 转义数据,但是如果您特别想将任何类型的空格转换为 %20,我想用正则表达式来做是有意义的。

编辑; 你问什么时候使用 Parallel ForEach,我会说“一般不要,因为你必须更加小心你使用它的上下文”,但是如果你让 Parallel.ForEach 做更多同步工作,这可能是有道理的:

    Parallel.ForEach(numbers, number =>
    {
        var value = Regex.Replace(number, @"\s+", "%20");

        var r = client.Get(url + value));

        //do something meaningful with r here, i.e. whatever ... is in your  foreach (var task in tasks)

    });
Run Code Online (Sandbox Code Playgroud)

但是请注意,如果您出于协调目的从主体内部对某些共享事物执行更新,则它需要是线程安全的

  • 无需并发收集;没有任何事情同时发生;它在“foreach”中顺序发生。最关键的部分是你不要在 foreach 中 `await` (你不会,但我是说“不要试图添加它”),否则请求的 IO *将*按顺序完成 (2认同)
  • @pinkfloydx33 无论如何,我将其CW了,因为它实际上只是可视化其他人在说什么,所以我不觉得这是“我的答案”..但是谢谢!:) (2认同)

pin*_*x33 5

您还没有显示它,所以我们只能猜测,但我假设tasks是一个List<>. 该集合类型不是线程安全的;您的并行循环可能会“覆盖”值。手动锁定列表或切换到线程安全集合,例如ConcurrentQueue<>

var tasks = new ConcurrentQueue<Task<string>>();

Parallel.ForEach(numbers, number =>
{
    var value = Regex.Replace(number, @"\s+", "%20");
    tasks.Enqueue(client.GetAsync(url + value));
});

await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);

foreach (var task in tasks)
{
   // whatever 
}
Run Code Online (Sandbox Code Playgroud)

也就是说,你的使用Parallel.ForEach是相当可疑的。您没有在循环内执行任何真正重要的事情。使用Parallel,特别是在适当锁定的情况下,可能会产生更高的开销,从而抵消您声称观察到的或通过并行化调用实现的任何潜在收益Regex。我会将其转换为普通foreach循环并预编译Regex以抵消(部分)其开销:

// in class
private static readonly Regex SpaceRegex = new Regex(@"\s+", RegexOptions.Compiled);

// in method
var tasks = new List<Task<string>>();

foreach (var number in numbers)
{
    var value = SpaceRegex.Replace(number, "%20");
    tasks.Add(client.GetAsync(url + value));
}

await Task.WhenAll(tasks).ConfigureAwait(false);

foreach (var task in tasks)
{
   // whatever 
}
Run Code Online (Sandbox Code Playgroud)

或者,根本不使用正则表达式。使用正确的 Uri 转义机制,它的好处不仅仅是修复空格:

// in class
private static readonly Regex SpaceRegex = new Regex(@"\s+", RegexOptions.Compiled);

// in method
var tasks = new List<Task<string>>();

foreach (var number in numbers)
{
    var value = SpaceRegex.Replace(number, "%20");
    tasks.Add(client.GetAsync(url + value));
}

await Task.WhenAll(tasks).ConfigureAwait(false);

foreach (var task in tasks)
{
   // whatever 
}
Run Code Online (Sandbox Code Playgroud)

请注意,那里有两种不同的方法。正确使用的方法取决于url和的值number。还有其他机制,例如方法HttpUtility.UrlEncode...但我认为这些是首选机制。