使用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更新代码。
是;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();
你应该记住的几个区别:
- 默认情况下,
Parallel
在ThreadPool
上运行代码,但它是可配置的。PLINQ独占使用ThreadPool
.
Parallel
默认具有无限的并行性(它使用ThreadPool
的所有可用线程)。PLINQ默认最多使用Environment.ProcessorCount
个线程。对于结果的顺序,PLINQ默认不保留顺序。如果希望保持顺序,可以附加AsOrdered
操作符。