List<T> 线程安全性

38

我正在使用以下代码

var processed = new List<Guid>();
Parallel.ForEach(items, item => 
{
    processed.Add(SomeProcessingFunc(item));
});

上述代码是否是线程安全的?处理过的列表有损坏的可能性吗?或者我应该在添加之前使用锁定?

var processed = new List<Guid>();
Parallel.ForEach(items, item => 
{
    lock(items.SyncRoot)
        processed.Add(SomeProcessingFunc(item));
});
谢谢。

1
你看过MSDN了吗?在这里:http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx#c9721fa0-1cd9-4a21-818c-98d164c9fc14 - R. Martinho Fernandes
1
请参见https://dev59.com/k1PTa4cB1Zd3GeqPlKnP。 - mellamokb
6
例子:该列表记录其元素数量。当添加一个元素时,它会将其放置在下一个位置并增加计数。那么就存在竞争条件:两个线程都可以添加一个元素,并且计数只增加了一个(一个元素在此过程中丢失)。 - R. Martinho Fernandes
@Martinho:非常棒的回复,让我明白了。谢谢。 - stackoverflowuser
我建议您使用已知类型而不是使用“var”。 - mozillanerd
显示剩余2条评论
6个回答

36

不安全!因为processed.Add是不安全的。你可以这样做:

items.AsParallel().Select(item => SomeProcessingFunc(item)).ToList();

请记住,Parallel.ForEach 主要用于对序列中的每个元素执行 命令式 操作。你所做的是进行映射:将序列的每个值进行投影。这就是 Select 的用途。 AsParallel 可以在大多数高效的线程中扩展它。

此代码正常工作:

var processed = new List<Guid>();
Parallel.ForEach(items, item => 
{
    lock(items.SyncRoot)
        processed.Add(SomeProcessingFunc(item));
});

但在多线程方面并没有意义。在每次迭代时进行lock会强制完全序列化执行,一堆线程将等待单个线程。


请查看 http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx 的末尾,其中有一个关于线程安全的主题,以便完整。 - rene
@rene:在那个地址的末尾添加“#c9721fa0-1cd9-4a21-818c-98d164c9fc14”,它将直接指向相关部分 ;) - R. Martinho Fernandes
谢谢回复。是否最好使用ConcurrentBag<T>,如此处所述:https://dev59.com/k1PTa4cB1Zd3GeqPlKnP - stackoverflowuser
请问您能否提供您的意见,关于使用AsParallel是否比使用ConcurrentBag<T>更好? - stackoverflowuser
1
@stackoverflowuser 是的,当你真正需要并发访问数据结构时,你应该使用ConcurrentBag<T>。但实际上,像 PLINQ 这样的高级库很少需要这样做。 - Andrey

8

1
ConcurrentQueue<T>ConcurrentBag<T>更可取。 - Theodor Zoulias

5

来自Jon Skeet的书C#深入浅出

作为.Net 4中Parallel Extensions的一部分,引入了几个新的集合类型在一个新的System.Collections.Concurrent命名空间中。这些集合类型旨在面对来自多个线程的并发操作时保证安全,并且锁定较少。

其中包括:

  • IProducerConsumerCollection<T>
  • BlockingCollection<T>
  • ConcurrentBag<T>
  • ConcurrentQueue<T>
  • ConcurrentStack<T>
  • ConcurrentDictionary<TKey, TValue>
  • 和其他类型

同意。请查看我的答案,其中使用了System.Collections.Concurrent命名空间中的类型。 - mellamokb

1
使用类型为 Something 的 ConcurrentBag。
var bag = new ConcurrentBag<List<Something>>;
var items = GetAllItemsINeed();
Parallel.For(items,i =>                          
   {
      bag.Add(i.DoSomethingInEachI());
   });

应该是 var bag = new ConcurrentBag<Something>(); - Artemious

1
作为 Andrey 的 答案 的替代方案:
items.AsParallel().Select(item => SomeProcessingFunc(item)).ToList();

你也可以写成

items.AsParallel().ForAll(item => SomeProcessingFunc(item));

这使得其背后的查询更加高效,因为不需要合并,MSDN。 确保SomeProcessingFunc函数是线程安全的。 我认为,但没有测试过,如果列表可以在其他线程中修改(添加或删除)元素,则仍需要锁定。

0

读取是线程安全的,但添加不是。您需要设置一个读写锁定来进行添加,因为添加可能会导致内部数组调整大小,从而破坏并发读取。

如果您可以保证在添加时数组不会调整大小,则可能在读取时安全地添加,但请不要引用我。

但实际上,列表只是数组的接口。


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