我想知道List<T>
是否是线程安全的,并且读到几个读者没问题,但是一个以上的作者可能会引起问题。因此,我编写了以下测试以查看实际发生的情况。
[TestClass]
public class ListConcurrency
{
[TestMethod]
public void MultipleWritersTest()
{
var taskCnt = 10;
var addCnt = 100;
var list = new List<object>();
var tasks = new List<Task>();
for (int i = 0; i < taskCnt; i++)
{
var iq = i;
tasks.Add(Task.Run(() =>
{
Console.WriteLine("STARTING : " + iq);
for (int j = 0; j < addCnt; j++)
{
try
{
list.Add(new object());
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("FINISHING: " + iq);
}));
}
Task.WhenAll(tasks).Wait();
Console.WriteLine("FINISHED: " + list.Count);
}
}
这是示例输出:
STARTING : 0
FINISHING: 0
STARTING : 1
FINISHING: 1
STARTING : 8
STARTING : 9
FINISHING: 9
FINISHING: 8
STARTING : 2
FINISHING: 2
STARTING : 7
STARTING : 3
FINISHING: 3
FINISHING: 7
STARTING : 4
FINISHING: 4
STARTING : 6
FINISHING: 6
STARTING : 5
FINISHING: 5
FINISHED: 979
我对两件事感到惊讶:
如果同时发生(期望和错误的项目计数),那将是有道理的……这仅仅是List<T>
展示其非线程安全性的方式吗?
编辑:我的开场白写得不好,我知道,List<T>
不是线程安全的(例如,用于迭代),但是我想看看如果以这种方式“滥用”会发生什么。正如我在下面的评论中所写,调试时结果(不会抛出异常)可能对其他人有用。
答案 0 :(得分:4)
如果选中source code of List,您会发现它在内部可以在数组上运行。 Add方法扩展数组大小并插入新项目:
_items
现在想象一下,您同时拥有10个大小和2个线程插入的数组-都将数组扩展为11个,一个线程插入了索引11,而其他覆盖了索引11的项目。这就是为什么列表计数为11的原因不是12,您将丢失一项。
答案 1 :(得分:2)
好的,让我们看一下Add
1 并考虑当多个线程访问它时会发生什么:
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
看起来不错。但是,让我们考虑一个特别不幸的线程在另一个线程设法执行第2行之前已经通过了此代码的第1行时发生了什么,导致_size
等于{{1} }。现在,我们不幸的线程将离开_items.Length
数组的末尾并引发异常。
因此,尽管您“证明”它不会抛出异常,但我发现一个明显的竞赛会导致在检查代码大约2分钟后导致竞赛。
1 来自reference source的代码,这当然意味着它可能与实际运行的代码并不完全相同,因为开发人员免费使用 更改其实现,仅遵守记录的保证。
答案 2 :(得分:1)
commission
不是线程安全的,您需要使用其他集合。您应该尝试使用List<T>
或文档中指定的here的任何其他收集类型。