【问题标题】:Why does List<>.Add from multiple threads lead to varying results? [duplicate]为什么 List<>.Add 来自多个线程会导致不同的结果? [复制]
【发布时间】:2018-07-27 09:13:13
【问题描述】:

我想知道,如果List&lt;T&gt; 是线程安全的并且读取多个读取器没有问题,但不止一个写入器可能会导致问题。所以我写了下面的测试来看看实际发生了什么。

[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&lt;T&gt; 展示其非线程安全性的方式吗?

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

【问题讨论】:

  • List&lt;T&gt; 不是线程安全的list.Add(new object());
  • 非线程安全的关键在于它经常导致无法在 100% 的时间内重现的错误。我似乎记得在过去看到它有时会设法取消引用空值,这会引发异常。但是这个测试的point是什么?您现在知道List&lt;T&gt; 不是线程安全的,那么为什么还要努力“证明”它呢?
  • 您在哪里发现 List 是线程安全的? Documentation(页面底部)声明It is safe to perform multiple read operations on a List&lt;T&gt;, but issues can occur if the collection is modified while it’s being read.,这就是你正在做的事情。
  • 做一些非线程安全的事情就像在十字路口闯红灯一样。有时什么都没有发生。有时会发生巨大的崩溃,每个人都死了。有时在这两个极端之间会发生其他不好的事情。结果永远不能保证是相同的。
  • 即使你发现它确实抛出了异常(有时它可能仍然会抛出异常,但你还没有证明它不会),这些信息也毫无意义因为它会抛出异常由于程序员错误。你不应该捕获这些异常,你应该修复你的代码。

标签: c# multithreading concurrency


【解决方案1】:

如果您检查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】:

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

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

    看起来不错。但是,让我们考虑一个特别倒霉的线程在另一个线程设法执行第 2 行之前已经超过该代码的第 1 行,导致_size 变得等于_items.Length 会发生什么。我们不幸的线程现在将离开_items 数组的末尾并抛出异常。

    因此,尽管您的“证明”表明它不会引发异常,但我发现了一个明显的竞争,在检查代码大约 2 分钟后会导致竞争。


    1取自reference source的代码当然意味着它可能与实际运行的代码不完全相同,因为开发人员是免费的改变他们的实现,只尊重记录的保证。

    【讨论】:

    • 哈...同时写多个答案 :)
    • @Reniuz - 是的,但我们在两种不同的比赛条件下比赛:-)。你的更新丢失了。我的状态不一致导致异常。
    【解决方案3】:

    List&lt;T&gt; 不是线程安全的,您需要使用不同的集合。您应该尝试使用 ConcurrentBag&lt;T&gt; 或文档中指定的任何其他集合类型 here

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-07
      • 1970-01-01
      • 2017-10-31
      • 2011-06-29
      • 2013-07-10
      相关资源
      最近更新 更多