在开始一个项目之前,我编写了一个简单的测试来比较(System.Collections.Concurrent)中的ConcurrentBag与锁定&列表的性能。我非常惊讶ConcurrentBag比用一个简单的List锁定要慢10倍以上。据我了解,当读者和作家是同一线程时,ConcurrentBag效果最佳。但是,我没有想到它的性能不会比传统锁差很多。
我已经用两个Parallel for循环对列表/包进行写入和读取操作进行了测试。但是,写入本身显示出巨大的差异:
private static void ConcurrentBagTest()
{
int collSize = 10000000;
Stopwatch stopWatch = new Stopwatch();
ConcurrentBag<int> bag1 = new ConcurrentBag<int>();
stopWatch.Start();
Parallel.For(0, collSize, delegate(int i)
{
bag1.Add(i);
});
stopWatch.Stop();
Console.WriteLine("Elapsed Time = {0}",
stopWatch.Elapsed.TotalSeconds);
}
在我的盒子上,这需要3-4秒的时间才能运行,而此代码为0.5-0.9秒:
private static void LockCollTest()
{
int collSize = 10000000;
object list1_lock=new object();
List<int> lst1 = new List<int>(collSize);
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
Parallel.For(0, collSize, delegate(int i)
{
lock(list1_lock)
{
lst1.Add(i);
}
});
stopWatch.Stop();
Console.WriteLine("Elapsed = {0}",
stopWatch.Elapsed.TotalSeconds);
}
正如我提到的,进行并发读取和写入对并发袋测试没有帮助。我是在做错什么,还是这个数据结构真的很慢?
[编辑]-我删除了任务,因为这里不需要它们(完整的代码有另一个任务需要阅读)
[编辑]
非常感谢您的回答。我很难选择“正确的答案”,因为这似乎是几个答案的组合。
正如Michael Goldshteyn所指出的,速度实际上取决于数据。
Darin指出,应该有更多的争用让ConcurrentBag更快,而Parallel.For不必启动相同数量的线程。要带走的一点是,不要在锁中不执行不需要的任何事情。在上述情况下,我看不到自己在锁内执行任何操作,除非可能是将值分配给temp变量。
另外,sixlettervariables指出,尽管我尝试以相反的顺序运行原始测试并且ConcurrentBag仍然较慢,但是碰巧正在运行的线程数也可能会影响结果。
我从启动15个任务开始进行了一些测试,结果取决于其他因素。但是,对于最多一百万次插入,ConcurrentBag的性能几乎与锁定列表一样好或比后者更好。超过一百万,锁定有时看起来会快得多,但是我的项目可能永远不会拥有更大的数据结构。
这是我运行的代码:
int collSize = 1000000;
object list1_lock=new object();
List<int> lst1 = new List<int>();
ConcurrentBag<int> concBag = new ConcurrentBag<int>();
int numTasks = 15;
int i = 0;
Stopwatch sWatch = new Stopwatch();
sWatch.Start();
//First, try locks
Task.WaitAll(Enumerable.Range(1, numTasks)
.Select(x => Task.Factory.StartNew(() =>
{
for (i = 0; i < collSize / numTasks; i++)
{
lock (list1_lock)
{
lst1.Add(x);
}
}
})).ToArray());
sWatch.Stop();
Console.WriteLine("lock test. Elapsed = {0}",
sWatch.Elapsed.TotalSeconds);
// now try concurrentBag
sWatch.Restart();
Task.WaitAll(Enumerable.Range(1, numTasks).
Select(x => Task.Factory.StartNew(() =>
{
for (i = 0; i < collSize / numTasks; i++)
{
concBag.Add(x);
}
})).ToArray());
sWatch.Stop();
Console.WriteLine("Conc Bag test. Elapsed = {0}",
sWatch.Elapsed.TotalSeconds);
最佳答案
让我问你一个问题:拥有一个不断添加到集合而从不读取的应用程序有多现实?这样的集合有什么用? (这不是一个纯粹的修辞问题。我可以想象有一些用途,例如,您仅在关机(用于日志记录)时或在用户请求时才从集合中读取内容。不过,我相信这些情况很少见。
这就是您的代码正在模拟。除了偶尔需要调整列表内部数组大小的情况外,调用List<T>.Add
将会非常快。但这很快就被所有其他添加所消除。因此,在这种情况下,您不太可能看到大量竞争,尤其是在具有甚至8个内核的个人PC上进行测试(正如您在某处的评论中所指出的那样)。也许您会在24核计算机之类的设备上看到更多争用,其中许多核可尝试同时从字面上添加到列表中。
尤其是在您从馆藏中读取内容时,争用很有可能蔓延。在foreach
循环(或LINQ查询,相当于幕后的foreach
循环)中,它们需要锁定整个操作,以便在迭代时无需修改集合。
如果您可以现实地重现这种情况,我相信您会看到ConcurrentBag<T>
比例比当前测试显示的要好得多。
更新:Here是我编写的一个程序,用于在上述场景中(多个作者,许多读者)比较这些集合。运行25个具有10000个集合大小的试验和8个阅读器线程,我得到以下结果:
花费529.0095 ms,将10000个元素添加到具有8个阅读器线程的List 中。
花费39.5237毫秒,将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
花费309.4475毫秒将10000个元素添加到具有8个阅读器线程的List 中。
花费81.1967毫秒将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
花费228.7669 ms,将10000个元素添加到具有8个阅读器线程的List 中。
花费164.8376毫秒将10000个元素添加到具有8个读取器线程的ConcurrentBag 中。
[...]
平均列表时间:176.072456毫秒。
平均布袋时间:59.603656毫秒。
因此很明显,这完全取决于您对这些集合的处理方式。