.ToList()线程安全

时间:2015-09-16 17:43:55

标签: c# .net multithreading thread-safety

我有以下情况:

  • 只有一个只能将项目添加到列表中
  • 可以读取项目的多个线程。

我知道当我们直接公开列表时,我会在枚举时遇到并发修改。

但是当我调用.ToList()时,它会创建一个List实例,并使用.NET Framework的Array.Copy函数复制所有项目。

你的意思是什么,当脏读的时候使用这种方法是否安全?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var container = new Container();
            Thread writer = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    container.Add(new Item("Item " + i));
                }
            });

            writer.Start();

            Thread[] readerThreads = new Thread[10];
            for (int i = 0; i < readerThreads.Length; i++)
            {
                readerThreads[i] = new Thread(() =>
                {
                    foreach (Item item in container.Items)
                    {
                        Console.WriteLine(item.Name);
                    }
                });
                readerThreads[i].Start();
            }

            writer.Join();

            for (int i = 0; i < readerThreads.Length; i++)
            {
                readerThreads[i].Join();
            }

            Console.ReadLine();
        }
    }

    public class Container
    {
        private readonly IList<Item> _items;

        public Container()
        {
            _items = new List<Item>();
        }

        public IReadOnlyList<Item> Items
        {
            get { return _items.ToList().AsReadOnly(); }
        }

        public void Add(Item item)
        {
            _items.Add(item);
        }
    }

    public class Item
    {
        private readonly string _name;

        public Item(string name)
        {
            _name = name;
        }

        public string Name
        {
            get { return _name; }
        }
    }
}

3 个答案:

答案 0 :(得分:4)

  

在脏读的时候使用这种方法是否安全?

简单回答 - 不,不是。

首先,该方法不保证按定义。即使您查看当前的实现(您不应该这样做),您也会看到它正在使用List<T>构造函数接受IEnumerable<T>。然后它检查ICollection<T>,如果是,则使用CopyTo方法或只迭代可枚举。两者都是不安全的,因为第一个依赖于CopyTo方法的(未知)实现,而第二个将在Add期间(标准行为)集合枚举器失效时接收异常。即使源是List<T>并使用您提到的Array方法http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,d2ac2c19c9cf1d44

Array.Copy(_items, 0, array, arrayIndex, _size);

仍然不安全,因为它可能会在_items之外调用_size_items.Length的副本,这当然会引发异常。只有当这个代码以某种方式以原子方式获得两个成员时,你的假设才是正确的,但事实并非如此。

所以,简单的不要这样做。

编辑:以上所有内容均适用于您的具体问题。但我认为对于你所解释的情况有一个简单的解决方案。如果需要单线程添加/多线程读取和可接受的过时读取,那么可以通过使用ImmutableList<T>包中的System.Collections.Immutable类来实现,如下所示:

public class Container
{
    private ImmutableList<Item> _items = ImmutableList<Item>.Empty;
    public IReadOnlyList<Item> Items { get { return _items; } }
    public void Add(Item item) { _items = _items.Add(item); }
}

答案 1 :(得分:3)

您的实现不是线程安全的,尽管每次访问<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <input type="text" id="txt" /> <select id="sel"> <option value="">Select</option> <option value=1>Domain Registration (.co.uk)</option> <option value=2>Domain Registration (.co)</option> <option value=3>Domain Registration (.com)</option> <option value=2>Domain Registration (.uk.com)</option> </select>属性时都会返回列表的全新副本。

考虑这种情况:

Items

当调用Container c = new Container(); // Code in thread1 c.Add(new Item()); // Code in thread2 foreach (var item in c.Items) { ... } 时,c.Items列表会在_items内枚举,以构建副本。此枚举与您自己执行的枚举没有区别。就像你的枚举一样,这个枚举不是线程安全的。

您可以通过添加锁对象并在添加到容器时或在请求容器中的项目副本时锁定它来使您的实现成为线程安全的:

ToList()

答案 2 :(得分:3)

我要做的是放弃当前的实施并切换到BlockingCollection

static void Main(string[] args)
{
    var container = new BlockingCollection<Item>();
    Thread writer = new Thread(() =>
    {
        for (int i = 0; i < 1000; i++)
        {
            container.Add(new Item("Item " + i));
        }
        container.CompleteAdding();
    });

    writer.Start();

    Thread[] readerThreads = new Thread[10];
    for (int i = 0; i < readerThreads.Length; i++)
    {
        readerThreads[i] = new Thread(() =>
        {
            foreach (Item item in container.GetConsumingEnumerable())
            {
                Console.WriteLine(item.Name);
            }
        });
        readerThreads[i].Start();
    }

    writer.Join();

    for (int i = 0; i < readerThreads.Length; i++)
    {
        readerThreads[i].Join();
    }

    Console.ReadLine();
}

BlockingCollection将做什么是为您提供线程安全队列(默认情况下,它使用ConcurrentQueue作为后备存储,您可以通过传递{{1}将行为更改为堆栈当队列为空等待更多数据显示时,它将阻止读取器上的ConcurrentStack循环。一旦制作人调用foreach,让所有读者停止阻止并退出他们的container.CompleteAdding()循环。

这确实会改变您的程序的行为,您的旧方式会给同一个foreach多个线程,这个新代码只会将项目插入到单个&#34;工人&#34;线。此实现还将为工作人员提供新项目,在工作人员进入其Item循环后插入到列表中,您的旧实现使用&#34;快照&#34;的列表,并在该快照上工作。我愿意打赌这是你真正想要的行为,但你没有意识到你的旧方式会重复处理。

如果您确实希望保留多个线程的旧行为全部使用&#34;快照时间&#34;列表(多个线程处理相同的项目,未处理快照后添加到列表中的新项目)切换到直接使用foreach而不是ConcurrentQueueBlockingCollection }及时创造一个&#34;时刻&#34;创建枚举器时队列的快照,并将该列表用于GetEnumerator()

foreach
相关问题