5

我在同时访问列表的站点上遇到了一些问题。这个列表保留了一个购物车,多次删除会使网站崩溃。 同步它们的最佳方法是什么? 一把锁就够了吗?锁定选项似乎很难看,因为代码遍布各处并且非常混乱。

更新:这是一个这样实现的列表: public class MyList : List< SomeCustomType> { }

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

任何的想法!

4

4 回答 4

7

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

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

于 2008-10-09T09:40:21.103 回答
4

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

于 2008-10-09T09:31:24.613 回答
4

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

lock (myList.SyncRoot) 
{
    // Access the collection.
}
于 2008-10-09T11:47:02.593 回答
0

@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,但如果集合本身应该代表一些有意义/重要的概念,这永远不够。

于 2012-02-21T09:48:49.250 回答