包含非null元素的列表最终包含null。同步问题?

时间:2010-04-26 13:20:13

标签: c# list asynchronous synchronization null

首先,对标题感到抱歉 - 我无法找出一个简短而清晰的标题。

问题在于:我有一个列表List<MyClass> list,我总是在其中添加新创建的MyClass实例,如下所示:list.Add(new MyClass())。我不会以任何其他方式添加元素。

但是,然后我用foreach遍历列表并发现有一些空条目。也就是说,以下代码:

foreach (MyClass entry in list)
    if (entry == null)
         throw new Exception("null entry!");

有时会抛出异常。 我应该指出list.Add(new MyClass())是从同时运行的不同线程执行的。我能想到的唯一考虑null条目的是并发访问。毕竟,List<>不是线程安全的。虽然我仍觉得奇怪的是它最终包含空条目,而不仅仅是在订购时没有提供任何保证。

你能想到其他任何原因吗?

另外,我不关心添加项目的顺序,我也不希望调用线程阻止等待添加项目。如果同步确实是问题,你能推荐一种简单的方法来异步调用Add方法,即创建一个委托,在我的线程运行其代码时处理它吗?我知道我可以为Add创建一个代理,并在其上调用BeginInvoke。这看起来合适吗?

感谢。


编辑:基于凯文建议的简单解决方案:

public class AsynchronousList<T> : List<T> {

    private AddDelegate addDelegate;
    public delegate void AddDelegate(T item);

    public AsynchronousList() {
        addDelegate = new AddDelegate(this.AddBlocking);
    }

    public void AddAsynchronous(T item) {
        addDelegate.BeginInvoke(item, null, null);
    }

    private void AddBlocking(T item) {
        lock (this) {
            Add(item);
        }
    }
}

我只需要控制Add操作,我只需要这个用于调试(它不会出现在最终产品中),所以我只想快速修复。

感谢大家的回答。

3 个答案:

答案 0 :(得分:8)

List<T>只能同时支持多个读者。如果要使用多个线程添加到列表中,则需要先锁定对象。实际上没有办法解决这个问题,因为没有锁定,你仍然可以让某人从列表中读取而另一个线程更新它(或者多个对象同时尝试更新它)。

http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx

您最好的选择可能是将列表封装在另一个对象中,并使该对象处理内部列表上的锁定和解锁操作。这样你就可以使你的新对象的“添加”方法异步,让调用对象以他们的快乐方式运行。任何时候你读它虽然你很可能仍然需要等待任何其他对象完成他们的更新。

答案 1 :(得分:4)

  

我唯一可以考虑考虑空条目的是并发访问。毕竟,List<>不是线程安全的。

基本上就是这样。我们被明确告知它不是线程安全的,所以我们不应该对并发访问会导致合同破坏行为感到惊讶。

至于为什么会出现这个特定问题,我们可以推测,因为List<>的私有实现是私有的(我知道我们有反射器和共享源 - 但原则上它是私有的)。假设实现涉及一个数组和一个“最后填充的索引”。假设“添加项目”看起来像这样:

  • 确保阵列足够大以容纳其他项目
  • 最后填充的索引&lt; - 最后填充的索引+ 1
  • array [last populated index] =传入项目

现在假设有两个线程调用Add。如果交错的操作序列最终如下:

  • 主题A:最后填充的索引&lt; - 最后填充的索引+ 1
  • 主题B:最后填充的索引&lt; - 最后填充的索引+ 1
  • 主题A:数组[最后填充的索引] =传入的项目
  • 主题B:数组[最后填充的索引] =传入项目

那么不仅数组中会有null,而且线程A试图添加的项目根本不会在数组中!

现在,我不知道知道以确定List<>内部如何处理它的内容。我有一半的内存与ArrayList,内部使用此方案;但实际上并不重要。我怀疑任何希望非并发运行的列表机制可以打破并发访问和足够“不幸”的操作交错。如果我们想要一个不提供它的API的线程安全,我们自己做一些工作 - 或者至少,如果我们穿上它时有时会破坏它,我们不应该感到惊讶“T

根据您的要求

  

我不希望调用线程阻止等待添加项目

我的第一个想法是多生产者 - 单一消费者队列,其中想要添加项目的线程是生产者,它将项目分配给队列异步,并且有一个消费者将项目从队列中取出并添加他们到适当的锁定列表。我的第二个想法是,这感觉好像比这种情况更重,所以我会让它考虑一下。

答案 2 :(得分:1)

如果您使用的是.NET Framework 4,则可以查看新的Concurrent Collections。当涉及到线程时,最好不要试图变得聪明,因为它很容易弄错。同步可能会影响性能,但是线程错误的影响也可能导致奇怪的,不常见的错误,这些错误很难追查。

如果您仍在使用Framework 2或3.5进行此项目,我建议您只需在lock语句中将调用包装到列表中。如果你担心Add的性能(你是否正在使用其他地方的列表执行一些长时间运行的操作?)那么你总是可以在一个锁中复制一个列表,并将该副本用于你之外的长期运行操作。锁。除非你有大量的线程,否则简单地阻止Adds本身应该不是性能问题。如果是这种情况,您可以尝试AakashM推荐的Multiple-Producer-Single-Consumer队列。