我有以下情况:
我知道当我们直接公开列表时,我会在枚举时遇到并发修改。
但是,当我调用.ToList()时,它会创建列表的一个实例,并使用.NET框架的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; }
}
}
}发布于 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 )
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); }
}发布于 2015-09-16 17:59:44
我要做的是放弃当前的实现,转而使用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作为后备存储,您可以通过将ConcurrentStack传递给构造函数将行为更改为堆栈),当队列为空等待更多数据出现时,该队列将阻塞读取器上的foreach循环。一旦生产者调用container.CompleteAdding(),允许所有的读取器停止阻塞并退出他们的foreach循环。
--这确实改变了程序的行为--,旧的方式会将相同的Item分配给多个线程,这段新代码只会给出插入到单个"worker“线程的项。此实现还将向工作人员提供新项,在工作人员进入其foreach循环后插入到列表中时,旧的实现将使用列表的“快照”并对该快照进行工作。我敢打赌,这是你真正想要的行为,但你没有意识到你的旧方式会重复处理。
如果您确实希望保留所有使用“时间快照”列表的多个线程的旧行为(多个线程处理相同的项,快照未处理后添加到列表中的新项)切换到直接使用ConcurrentQueue而不是BlockingCollection,则在创建枚举器时,GetEnumerator()将为队列创建一个“瞬间”快照,并将该列表用于foreach。
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();
}发布于 2015-09-16 17:55:20
您的实现并不是线程安全的,尽管每次访问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()中枚举,以便构造副本。此枚举与您自己执行的枚举没有什么不同。就像枚举一样,此枚举并不是线程安全的.
您可以通过添加一个锁对象,并在添加到容器或请求容器中的项的副本时锁定它,从而使您的实现线程安全:
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);
}
}
}https://stackoverflow.com/questions/32615290
复制相似问题