如何同步对ASP.NET中使用的List 的访问?

我在具有并发访问列表的站点上遇到了一些问题。 此列表保留一个项目的购物车,并且多次删除都会导致网站崩溃。 哪个是同步它们的最佳方法? 锁够了吗? 锁定选项似乎很难看,因为代码遍布整个地方并且非常混乱。

更新:这是一个如下所示的列表:public class MyList:List {}

这是一个遗留站点,因此不允许进行太多修改。 我应该如何重构这个以便在迭代时安全锁定?

任何的想法!

请求之间是否共享内存列表? 即使你已经锁定,这听起来也是一个潜在的问题原因。 通常应避免使用可变共享集合,即IME。

请注意,如果您决定进行同步,则需要执行诸如锁定列表以及迭代它的整个过程以及其他复合操作之类的操作。 只是锁定每个特定的访问权限是不够的。

使用lock是同步对generics集合的访问的正确方法。 对集合的访问遍布整个地方,您可以创建一个包装类来包装对集合的访问,从而减少交互表面。 然后,您可以在包装对象中引入同步。

是的,您必须像这样使用List.SyncRoot锁定:

lock (myList.SyncRoot) { // Access the collection. } 

@Samuel这正是重点:你不能仅通过修改现有的类来纠正这样的问题。 没关系, class extends List ,这与MS方式一致,主要是避免使用虚拟方法(只有MS打算覆盖它们才能使它成为虚拟方法,这大部分都是有意义的,但在你需要的情况下可能会让生活变得更加困难一个黑客)。 即使它嵌入了List并且对它的所有访问都经过了可以更改的代码,您也不能简单地修改此代码添加锁来解决问题。

为什么? 嗯,迭代器一件事。 不能仅在每次访问集合之间修改集合。 在整个枚举期间无法修改它。

实际上,同步是一个复杂的问题,取决于列表的真正含义以及系统中任何时候需要的类型。

为了说明,这是一个滥用IDisposable的黑客。 这将嵌入列表并重新声明在某处使用的列表的任何function,以便可以同步对列表的访问。 另外,它没有实现IEnumerable。 相反,获取列表上的枚举访问权限的唯一方法是通过返回一次性类型的方法。 此类型在创建时进入监视器,并在处理时再次退出。 这可确保在迭代期间无法访问列表。 但是,这仍然不够,因为我的示例用法将说明。

首先是黑客名单:

public class MyCollection { object syncRoot = new object(); List list = new List();

 public void Add(T item) { lock (syncRoot) list.Add(item); } public int Count { get { lock (syncRoot) return list.Count; } } public IteratorWrapper GetIteratorWrapper() { return new IteratorWrapper(this); } public class IteratorWrapper : IDisposable, IEnumerable { bool disposed; MyCollection c; public IteratorWrapper(MyCollection c) { this.c = c; Monitor.Enter(c.syncRoot); } public void Dispose() { if (!disposed) Monitor.Exit(c.syncRoot); disposed = true; } public IEnumerator GetEnumerator() { return c.list.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } 

}

然后使用它的控制台应用程序:

class Program { static MyCollection strings = new MyCollection();

 static void Main(string[] args) { new Thread(adder).Start(); Thread.Sleep(15); dump(); Thread.Sleep(125); dump(); Console.WriteLine("Press any key."); Console.ReadKey(true); } static void dump() { Console.WriteLine(string.Format("Count={0}", strings.Count).PadLeft(40, '-')); using (var enumerable = strings.GetIteratorWrapper()) { foreach (var s in enumerable) Console.WriteLine(s); } Console.WriteLine("".PadLeft(40, '-')); } static void adder() { for (int i = 0; i < 100; i++) { strings.Add(Guid.NewGuid().ToString("N")); Thread.Sleep(7); } } 

}

注意“转储”方法:它访问Count,这是无意义地锁定列表以试图使其“线程安全”,然后遍历项目。 但是Count getter(锁定,获取计数,然后释放)和using语句之间存在竞争条件。 所以它可能不会抛弃它所做的事情的数量。

在这里,它可能无关紧要。 但是如果代码做了类似的事情呢:

 var a = new string[strings.Count]; for (int i=0; i < strings.Count; i++) { ... } 

甚至更可能搞砸了:

 var n = strings.Count; var a = new string[n]; for (int i=0; i < n; i++) { ... } 

如果项目同时添加到列表中,前者将会爆炸。 如果从列表中删除项目,后者不会很好。 在这两种情况下,代码的语义可能不会受到列表更改的影响,即使更改不会导致代码崩溃。 在第一种情况下,可能会从列表中删除项目,导致数组无法填充。 随后由于数组中的空值而导致代码中远程删除的内容崩溃。

从中得出的教训是:共享状态可能非常棘手。 您需要一个前期计划,您需要制定一个策略,以确保您的意义是正确的。

在每种情况下,只有确保锁定跨越所有相关操作才能实现正确的操作。 中间可能存在许多其他语句,并且锁可以跨越许多方法调用和/或涉及许多不同的对象。 没有灵丹妙药可以仅通过修改集合本身来解决这些问题,因为正确的同步取决于集合的含义 。 像readonly对象的缓存这样的东西可能只能在add/remove/lookup进行同步,但如果集合本身应该代表一些有意义/重要的概念,那么这将永远不够。