C# Parallel.ForEach和Task.WhenAll有时返回比预期少的值

3

我有这个:

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)
{
  ...
}

有时在进入foreach(var task in tasks)时返回的任务较少,但经过几次请求后,开始返回所有任务。

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

顺便说一下,我正在使用Parallel.ForEach,因为每个client.GetAsync(url + value)都是对外部API的请求,其独特之处在于其延迟SLA低于99%的请求1秒

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

是否有办法保证始终返回所有任务?

谢谢


7
我猜你正在使用 System.Collections.Generic.List 来存储 tasks。这个集合是非线程安全的,你必须使用线程安全的集合。请参阅 System.Collections.Concurrent 命名空间。 - Alexander Petrov
1
我认为你不需要使用 Parallel.ForEach。使用异步方式,文档将自动并行下载。 - vc 74
1
Parallel.ForEach 的意义是什么?你在其中没有执行任何工作。只需使用普通的 foreach 循环将所有任务添加到列表中即可。这样,您就不会遇到上面评论中描述的问题。 - pinkfloydx33
1
不,你可以将任务收集到本地列表中,然后调用 WhenAll - vc 74
1
你没有等待client.GetAsync,所以它应该几乎立即返回,我不知道你的说法有什么意义。放弃Parallel或切换到线程安全集合(这将具有自己的最小影响)。 - pinkfloydx33
显示剩余14条评论
2个回答

8

是否有一种方法可以保证始终返回所有任务?

评论中的几个人指出,假定numbers是非线程安全的List,则应该执行此操作:

    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)
    {
      ...
    }

并行化创建下载任务似乎没有什么明显的好处;这个过程非常快速。等待下载完成是在WhenAll中完成的。

提示:有各种更复杂的方法可以为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)

    });

但是请注意,如果您正在从内部执行更新以协调某些共享内容,则需要使其线程安全。


2
不需要并发集合;没有任何并发操作;它在foreach中按顺序执行。最关键的部分是不要在foreach中使用await(虽然你没有,但我要说“不要试图添加它”),否则请求的IO将会被顺序执行。 - Caius Jard
顺便问一下,你能简单地告诉我什么时候应该使用Parallel foreach吗? - MarchalPT
刚用手机打了五分钟的答案,所以没有看到你的回答。我留下我的答案并给您点赞。 - pinkfloydx33
2
@pinkfloydx33 不过我还是 CW 了,因为它只是在可视化大家所说的内容,所以我觉得这不算是“我的答案”..不过还是谢谢你! :) - Caius Jard
1
@MarchalPT 当你需要处理CPU密集型任务并且想要利用机器的多个核心来加速时,你应该使用Parallel.ForEach。这种方法有点原始,具有问题的默认并行度,并且通常不太可能让你轻松成功,除非你非常了解自己在做什么。一个更安全的并行工作工具是PLINQ。它类似于LINQ,但以.AsParallel()开头。例如:Task[] tasks = urls.AsParallel().AsOrdered().Select(url => client.GetAsync(url)).ToArray(); - Theodor Zoulias
显示剩余2条评论

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 
}

话虽如此,你对 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 
}

或者干脆不使用正则表达式。使用适当的Uri转义机制,这将带来更多好处,不仅可以修复空格:

var value = Uri.EscapeDataString(number);
// or
var fullUri = Uri.EscapeUriString(url + number);

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


喜欢你用正则表达式的方式。 - MarchalPT
现在要尝试一下,使用https://开头的URL是否有效? - MarchalPT
1
@MarchalPT 请参考以下示例:https://tio.run/##bc29DsIgFAXgvU9BOpUYIeDPIHFSN7fGOCMhSlKg4VKTPj1CipNuNyfnfFfBWoFJ6S0DcpN96ICOqGV8g7a7fSuakk9hKOErxhEOlNqZgImaKG9pbiwVlhv9DFFbcguGXEDJUeerj8G4Z1eIVX2AkWhO3oEfNLmHLF2N093E8Nfi2VoGP@JZRlnJiv21OBYpfQA - pinkfloydx33
仅因建议使用非常专业化ConcurrentBag<T>类作为容器而对其进行投票否定,该类旨在用于极其罕见的混合生产者-消费者场景。存储Parallel.ForEach循环结果的更好选择是ConcurrentQueue<T> - Theodor Zoulias
@TheodorZoulias 现在开心了吗? - pinkfloydx33
是的,感觉好多了!我已经对ConcurrentBag<T>类发起了我的小个人战争,这个笨重的“并发List<T>”冒充者,每一个小胜利都很重要。 - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接