为什么来自多个线程的List <>。Add会导致不同的结果?

时间:2018-07-27 09:13:13

标签: c# multithreading concurrency

我想知道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

我对两件事感到惊讶:

  1. 多次运行测试表明,有时结果列表计数不是预期的1000(= 10 x 100),而是更少。
  2. 添加过程中没有例外。

如果同时发生(期望和错误的项目计数),那将是有道理的……这仅仅是List<T>展示其非线程安全性的方式吗?

编辑:我的开场白写得不好,我知道List<T>不是线程安全的(例如,用于迭代),但是我想看看如果以这种方式“滥用”会发生什么。正如我在下面的评论中所写,调试时结果(不会抛出异常)可能对其他人有用。

3 个答案:

答案 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的任何其他收集类型。