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

时间:2008-10-09 09:27:38

标签: c# asp.net .net multithreading access-synchronization

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

更新: 这是一个像这样实现的列表: public class MyList:List&lt; SomeCustomType&GT; {}

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

任何想法!

4 个答案:

答案 0 :(得分:7)

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

请注意,如果你决定同步,你需要做一些事情,比如锁定整个迭代过程和其他复合操作的列表。只是锁定每个特定的访问权限是不够的。

答案 1 :(得分:4)

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

答案 2 :(得分:4)

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

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

答案 3 :(得分: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();
    }
}

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语句之间存在竞争条件。所以它可能不会抛弃它所做的事情的数量。

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

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);
    }
}

甚至更可能搞砸了:

}

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

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

在每种情况下,只有确保锁定涵盖所有相关操作,才能实现正确的操作。中间可能存在许多其他语句,并且锁可以跨越许多方法调用和/或涉及许多不同的对象。没有灵丹妙药可以仅通过修改集合本身来修复这些问题,因为正确的同步取决于集合的含义。类似于只读对象的缓存之类的东西可能只能与var a = new string[strings.Count]; for (int i=0; i < strings.Count; i++) { ... } 同步,但如果集合本身应该代表一些有意义/重要的概念,那么这将永远不够。