代码之家  ›  专栏  ›  技术社区  ›  mike

为什么list<>。从多个线程添加会导致不同的结果?

  •  0
  • mike  · 技术社区  · 8 年前

    我在想,如果 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. 添加期间未发生异常。

    如果两者都会发生(预期和错误的项目计数),这将是有意义的…就是这样吗 列表& T; 证明它的非线程安全性?

    编辑 我的开场白措词很差,我 知道 那个 列表& T; 不是线程安全的(例如用于迭代),但是我想看看如果它以这种方式被“滥用”会发生什么。正如我在下面的评论中所写,调试时结果(不会抛出异常)可能对其他人有用。

    3 回复  |  直到 8 年前
        1
  •  4
  •   Renatas M.    8 年前

    如果你检查 source code of List 您将看到它在内部对数组进行操作。add方法扩展数组大小并插入新项:

    // Adds the given object to the end of this list. The size of the list is
    // increased by one. If required, the capacity of the list is doubled
    // before adding the new element.
    //
    public void Add(T item) 
    {
       if (_size == _items.Length) EnsureCapacity(_size + 1);
          _items[_size++] = item;
       _version++;
     }
    

    现在假设您同时有10个和2个线程插入的数组-都将数组扩展为11个,在索引11处有一个线程插入,在索引11处有其他覆盖项。这就是为什么你会得到11而不是12的清单,你会失去一个项目。

        2
  •  2
  •   Damien_The_Unbeliever    8 年前

    好吧,让我们看看 Add 一 并考虑当多个线程访问它时会发生什么:

    public void Add(T item) {
        if (_size == _items.Length) EnsureCapacity(_size + 1);
        _items[_size++] = item;
        _version++;
    }
    

    看起来不错。但是让我们考虑一下,在另一个线程成功执行第2行之前,一个特别不走运的线程已经超过了这段代码的第1行,从而导致 _size 变得平等 到 _items.Length . 我们的不幸线现在要走到 _items 数组并引发异常。

    所以,尽管你“证明”它不会抛出异常,我还是发现了一个明显的竞赛,在检查代码大约2分钟后,会导致一个。


    一 代码取自 reference source 这当然意味着它可能与实际运行的代码不完全相同,因为开发人员 自由的 改变他们的实施,只尊重 文件化的 保证。

        3
  •  1
  •   Fabulous    8 年前

    List<T> 不是threadsafe,您需要使用不同的集合。你应该尝试使用 ConcurrentBag<T> 或指定的任何其他集合类型 here 在文档中。