使用Parallel.ForEach()是线程安全的吗?

本文关键字:安全 线程 Parallel ForEach 使用 | 更新日期: 2023-09-27 17:49:28

基本上,我正在处理这个:

var data = input.AsParallel();
List<String> output = new List<String>();
Parallel.ForEach<String>(data, line => {
    String outputLine = ""; 
    // ** Do something with "line" and store result in "outputLine" **
    // Additionally, there are some this.Invoke statements for updating UI
    output.Add(outputLine);
});

Input是一个List<String>对象。ForEach()语句对每个值做一些处理,更新UI,并将结果添加到output List中。这本身有什么问题吗?

指出:

  • 输出顺序不重要

更新:

根据我得到的反馈,我在output.Add语句中添加了手动lock,以及UI更新代码。

使用Parallel.ForEach()是线程安全的吗?

是;List<T>不是线程安全的,所以从任意线程(很可能同时)添加到它是注定要失败的。您应该使用线程安全列表,或者手动添加锁定。或者可能有一个Parallel.ToList

同样,如果它很重要:插入顺序将不保证。

这个版本安全的,但是

var output = new string[data.Count];
Parallel.ForEach<String>(data, (line,state,index) =>
{
    String outputLine = index.ToString();
    // ** Do something with "line" and store result in "outputLine" **
    // Additionally, there are some this.Invoke statements for updating UI
    output[index] = outputLine;
});

这里我们使用index来更新每个并行调用的不同数组索引

这有什么内在的错误吗?

是的,一切。这些都不安全。同时在多个线程上更新列表是不安全的,并且你不能从UI线程以外的任何线程更新UI。

关于List<T>的线程安全,文档是这样说的:

此类型的Public static(在Visual Basic中共享)成员是线程安全的。不能保证任何实例成员都是线程安全的。

一个List(Of T)可以同时支持多个reader,只要集合不被修改。在集合中枚举本质上不是线程安全的过程。在枚举与一个或多个写访问竞争的罕见情况下,确保线程安全的唯一方法是在整个枚举期间锁定集合。要允许多个线程访问集合进行读写,必须实现自己的同步。

因此,output.Add(outputLine)不是线程安全的,您需要自己确保线程安全,例如,通过在lock语句中包装add操作。

当您需要并行操作的结果时,PLINQ比Parallel类更方便。通过将input转换为ParallelQuery<T>,您开始得很好:

ParallelQuery<string> data = input.AsParallel();

…然后你把data喂给Parallel.ForEach,它把它当作标准的IEnumerable<T>。所以AsParallel()被浪费了。它没有提供任何并行化,只有开销。以下是使用PLINQ的正确方法:

List<string> output = input
    .AsParallel()
    .Select(line =>
    {
        string outputLine = ""; 
        // ** Do something with "line" and store result in "outputLine" **
        return outputLine;
    })
    .ToList();

你应该记住的几个区别:

  1. 默认情况下,ParallelThreadPool上运行代码,但它是可配置的。PLINQ独占使用ThreadPool .
  2. Parallel默认具有无限的并行性(它使用ThreadPool的所有可用线程)。PLINQ默认最多使用Environment.ProcessorCount个线程。

对于结果的顺序,PLINQ默认不保留顺序。如果希望保持顺序,可以附加AsOrdered操作符。