我在同时访问列表的站点上遇到了一些问题。这个列表保留了一个购物车,多次删除会使网站崩溃。 同步它们的最佳方法是什么? 一把锁就够了吗?锁定选项似乎很难看,因为代码遍布各处并且非常混乱。
更新:这是一个这样实现的列表: public class MyList : List< SomeCustomType> { }
这是一个旧站点,因此不允许对其进行太多修改。我应该如何重构它以便在迭代它时安全锁定?
任何的想法!
我在同时访问列表的站点上遇到了一些问题。这个列表保留了一个购物车,多次删除会使网站崩溃。 同步它们的最佳方法是什么? 一把锁就够了吗?锁定选项似乎很难看,因为代码遍布各处并且非常混乱。
更新:这是一个这样实现的列表: public class MyList : List< SomeCustomType> { }
这是一个旧站点,因此不允许对其进行太多修改。我应该如何重构它以便在迭代它时安全锁定?
任何的想法!
那么这个内存列表是否在请求之间共享?即使您已经锁定,这听起来也是一个潜在的问题原因。通常应避免使用可变共享集合,IME。
请注意,如果您决定进行同步,则需要在整个迭代过程中锁定列表以及其他复合操作。仅仅锁定每个特定的访问权限是不够的。
使用锁是同步访问泛型集合的正确方法。对集合的访问遍布各处,您可以创建一个包装类来包装对集合的访问,从而减少交互面。然后,您可以在包装对象中引入同步。
是的,您必须像这样使用List.SyncRoot锁定:
lock (myList.SyncRoot)
{
// Access the collection.
}
@Samuel 这正是重点:您不能仅通过修改现有类来纠正这样的问题。没关系,class extends List<T>
与 MS 方式保持一致的 . 大部分都避免使用虚拟方法(只有在 MS 打算让我们覆盖的地方,它们才会使其成为虚拟方法,这主要是有道理的,但在您有点需要破解的情况下会使生活变得更加困难)。即使它嵌入了List<T>
并且所有对它的访问都通过您可以更改的代码,您也不能简单地修改此代码添加锁来解决问题。
为什么?好吧,一件事的迭代器。不能仅仅在每次访问集合之间不修改集合。在整个枚举过程中不能修改。
实际上,同步是一件复杂的事情,它取决于列表的真正含义以及系统中需要始终为真的事物类型。
为了说明,这里有一个滥用 IDisposable 的黑客。这将嵌入列表并重新声明在某处使用的列表的任何功能,以便可以同步对列表的访问。此外,它没有实现 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<T>
{
bool disposed;
MyCollection<T> c;
public IteratorWrapper(MyCollection<T> c) { this.c = c; Monitor.Enter(c.syncRoot); }
public void Dispose() { if (!disposed) Monitor.Exit(c.syncRoot); disposed = true; }
public IEnumerator<T> 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++) { ... }
如果项目同时添加到列表中,前者会爆炸。如果从列表中删除项目,后者的情况不会很好。在这两种情况下,代码的语义可能不会不受列表更改的影响,即使更改不会导致代码崩溃。在第一种情况下,可能项目已从列表中删除,导致数组未填充。随后,由于数组中的空值,代码中的某些内容崩溃了。
从中吸取的教训是:共享状态可能非常棘手。你需要一个前期计划,你需要有一个策略来确保你的意思是正确的。
在每种情况下,只有确保锁跨越所有相关操作,才能实现正确操作。两者之间可能有许多其他语句,并且锁可能跨越许多方法调用和/或涉及许多不同的对象。没有什么灵丹妙药可以仅仅通过修改集合本身来解决这些问题,因为正确的同步取决于集合的含义。诸如只读对象缓存之类的东西可能只能与 同步add/remove/lookup
,但如果集合本身应该代表一些有意义/重要的概念,这永远不够。