多个线程将元素添加到一个列表.为什么列表中的项目总是少于预期?

Cui*_*崔鹏飞 12 c# parallel-processing concurrency multithreading task-parallel-library

以下代码解释了我的问题.我知道列表不是线程安全的.但是这个潜在的"真正"原因是什么?

    class Program
{
    static void Main(string[] args)
    {
        List<string> strCol = new List<string>();

        for (int i = 0; i < 10; i++)
        {
            int id = i;
            Task.Factory.StartNew(() =>
            {
                AddElements(strCol);
            }).ContinueWith((t) => { WriteCount(strCol, id.ToString()); });
        }

        Console.ReadLine();
    }

    private static void WriteCount(List<string> strCol, string id)
    {
        Console.WriteLine(string.Format("Task {0} is done. Count: {1}. Thread ID: {2}", id, strCol.Count, Thread.CurrentThread.ManagedThreadId));
    }

    private static void AddElements(List<string> strCol)
    {
        for (int i = 0; i < 20000; i++)
        {
            strCol.Add(i.ToString());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Mac*_*iej 21

我将跳过明显的答案"列表不是线程安全的" - 你已经知道了.

列表项保存在内部数组中.将项目添加到List时,至少有两个阶段(从逻辑的角度来看).首先,List获取一个索引,指示新项目的放置位置.它使用此索引将新项目放入数组中.然后它递增索引,这是第二阶段.如果第二个(或第三个,第四个......)线程同时添加新项目,则可能会有两个(3,4,...)新项目放入索引之前的同一个数组位置由第一个线程递增.项目被覆盖并丢失.

添加新项目和递增索引的内部操作必须始终一次完成,以使列表成为线程安全的.那就是所谓的关键部分.这可以通过锁来实现.

希望这能解释一下.


Ree*_*sey 13

这是因为List<T>不是线程安全的.

您应该为此使用线程安全集合,例如System.Collections.Concurrent中的一个集合.否则,你将需要同步所有访问权限List<T>(即:将每个Add调用放在一个锁中),这将完全打不过使用多个线程调用它的目的,因为在这种情况下你没有做任何其他工作.

  • @CuiPengFei当调用List.Add时,它只是检查要写入底层数组中的哪个元素...它有点像"获取当前大小","将[size]的值设置为新值","设置目前的规模".如果另一个线程在第一个线程的"Set"发生时启动,它们都将最终写入相同的元素,并且设置整体大小相同,并且一个值将从结果中"消失". (3认同)
  • @CuiPengFei"真正的"原因很可能是在没有锁定的情况下进行的操作(数据副本,数组调整大小等),假设开发人员没有同时从多个线程与`List`进行交互.使`List`线程安全所需的额外同步会显着影响性能,因此BCL设计人员选择省略它并记录缺少线程安全性,这样如果您需要线程安全访问,您可以自己在列表中构建它.与Java的`ArrayList`和许多其他`List`类相同. (2认同)