首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >.ToList()线程安全

.ToList()线程安全
EN

Stack Overflow用户
提问于 2015-09-16 17:43:55
回答 3查看 5.5K关注 0票数 8

我有以下情况:

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

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

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

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

代码语言:javascript
复制
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; }
        }
    }
}
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2015-09-16 18:09:59

如果脏读可以的话,使用这种方法安全吗?

简单的回答-不,不是。

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

代码语言:javascript
复制
Array.Copy(_items, 0, array, arrayIndex, _size);

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

所以简单点别那么做。

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

代码语言:javascript
复制
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); }
}
票数 7
EN

Stack Overflow用户

发布于 2015-09-16 17:59:44

我要做的是放弃当前的实现,转而使用BlockingCollection

代码语言:javascript
复制
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作为后备存储,您可以通过将ConcurrentStack传递给构造函数将行为更改为堆栈),当队列为空等待更多数据出现时,该队列将阻塞读取器上的foreach循环。一旦生产者调用container.CompleteAdding(),允许所有的读取器停止阻塞并退出他们的foreach循环。

--这确实改变了程序的行为--,旧的方式会将相同的Item分配给多个线程,这段新代码只会给出插入到单个"worker“线程的项。此实现还将向工作人员提供新项,在工作人员进入其foreach循环后插入到列表中时,旧的实现将使用列表的“快照”并对该快照进行工作。我敢打赌,这是你真正想要的行为,但你没有意识到你的旧方式会重复处理。

如果您确实希望保留所有使用“时间快照”列表的多个线程的旧行为(多个线程处理相同的项,快照未处理后添加到列表中的新项)切换到直接使用ConcurrentQueue而不是BlockingCollection,则在创建枚举器时,GetEnumerator()将为队列创建一个“瞬间”快照,并将该列表用于foreach

代码语言:javascript
复制
static void Main(string[] args)
{
    var container = new ConcurrentQueue<Item>();
    Thread writer = new Thread(() =>
    {
        for (int i = 0; i < 1000; i++)
        {
            container.Enqueue(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)
            {
                Console.WriteLine(item.Name);
            }
        });
        readerThreads[i].Start();
    }

    writer.Join();

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

    Console.ReadLine();
}
票数 4
EN

Stack Overflow用户

发布于 2015-09-16 17:55:20

您的实现并不是线程安全的,尽管每次访问Items属性时都会返回一个全新的列表副本。

考虑一下这种情况:

代码语言:javascript
复制
Container c = new Container();

// Code in thread1
c.Add(new Item());

// Code in thread2
foreach (var item in c.Items) {
    ...
}

当调用c.Items时,_items列表将在ToList()中枚举,以便构造副本。此枚举与您自己执行的枚举没有什么不同。就像枚举一样,此枚举并不是线程安全的.

您可以通过添加一个锁对象,并在添加到容器或请求容器中的项的副本时锁定它,从而使您的实现线程安全:

代码语言:javascript
复制
public class Container {
    private readonly IList<Item> _items;
    private readonly synclock = new object();

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

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

    public void Add(Item item) {
        lock (synclock) {
            _items.Add(item);
        }
    }
}
票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/32615290

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档