我需要在List上使用一些线程安全操作。
通常我只是简单地使用:
lock(List<T>){
List<T>.Add();
List<T>.Remove();
}
我也知道还有另一种方法,使用ConcurrentBag<T>
。但我不知道哪个更快或任何其他不同。
UPDATE1:
有些人建议我使用ConcurrentBag,因为这样更安全。但我担心它会让我的操作更慢。
我只需要很多线程需要在List中添加或删除一些对象,我只是想知道更好的性能方法。这就是我问的原因。
答案 0 :(得分:6)
只需尝试一下,您就可以轻松衡量不同方法的效果!这就是我刚刚得到的:
lock list: 2,162s ConcurrentBag: 7,264s
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
public class Test
{
public const int NumOfTasks = 4;
public const int Cycles = 1000 * 1000 * 4;
public static void Main()
{
var list = new List<int>();
var bag = new ConcurrentBag<int>();
Profile("lock list", () => { lock (list) list.Add(1); });
Profile("ConcurrentBag", () => bag.Add(1));
}
public static void Profile(string label, Action work)
{
var s = new Stopwatch();
s.Start();
List<Task> tasks = new List<Task>();
for (int i = 0; i < NumOfTasks; ++i)
{
tasks.Add(Task.Factory.StartNew(() =>
{
for (int j = 0; j < Cycles; ++j)
{
work();
}
}));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(string.Format("{0}: {1:F3}s", label, s.Elapsed.TotalSeconds));
}
}
答案 1 :(得分:3)
请勿使用ConcurrentBag<T>
替换锁定的List<T>
,除非您确定了线程的访问模式,因为它使用了线程本地存储。< / p>
MSDN讨论了首选用法:
&#34; ConcurrentBag是一个线程安全的包实现,针对同一个线程生成和使用存储在包中的数据的情况进行了优化。&#34;
同样重要的是要注意List<T>
已订购且ConcurrentBag<T>
无序。如果您不关心收藏中的订单,我会使用ConcurrentQueue<T>
。
关于性能,以下是ConcurrentBag<T>
的一些代码。但要考虑的主要事情是,如果你执行Take
并且你的线程本地存储空了,它将从其他昂贵的线程中窃取。
当它需要窃取它锁定时。另请注意,它可以在一个Take
上锁定多次,因为TrySteal
可能会失败并从Steal
调用多次(未显示)。
private bool TrySteal(ConcurrentBag<T>.ThreadLocalList list, out T result, bool take)
{
lock (list)
{
if (this.CanSteal(list))
{
list.Steal(out result, take);
return true;
}
result = default (T);
return false;
}
}
CanSteal
期间也可能有等待旋转。
private bool CanSteal(ConcurrentBag<T>.ThreadLocalList list)
{
if (list.Count <= 2 && list.m_currentOp != 0)
{
SpinWait spinWait = new SpinWait();
while (list.m_currentOp != 0)
spinWait.SpinOnce();
}
return list.Count > 0;
}
最后,即使添加也会导致锁定。
private void AddInternal(ConcurrentBag<T>.ThreadLocalList list, T item)
{
bool lockTaken = false;
try
{
Interlocked.Exchange(ref list.m_currentOp, 1);
if (list.Count < 2 || this.m_needSync)
{
list.m_currentOp = 0;
Monitor.Enter((object) list, ref lockTaken);
}
list.Add(item, lockTaken);
}
finally
{
list.m_currentOp = 0;
if (lockTaken)
Monitor.Exit((object) list);
}
}
答案 2 :(得分:2)
List
操作add
和remove
是O(n),这意味着锁定的持续时间取决于列表的大小。列表越大,您的并发性就越少。但是,如果您总是添加到最后并从末尾删除,则实际上有一个堆栈。在这种情况下,add
和remove
操作为O(1),您将拥有更短的锁。
ConcurrentBag
被实现为链接列表的链接列表(每个线程一个。操作add
和take
是O(1),并且在一般情况下不需要锁定。通常可以避免锁定的事实意味着它可能更快。