【问题标题】:My list is not thread safe even though I lock it即使我锁定它,我的列表也不是线程安全的
【发布时间】:2012-06-07 09:24:31
【问题描述】:

我得到:

"collection was modified enumeration operation may not execute" at this line:

foreach (Message msg in queue)

过了一会儿。

我必须使用 .NET 2.0。

我对名为“queue”的私有List进行的两个操作如下:

// 1st function calls this
lock (queue)
{
      queue.Add(msg);
}

// 2nd function calls this
lock (queue)
{
      using (StreamWriter outfile = new StreamWriter("path", true)
      {
             foreach (Message msg in queue) // This is were I get the exception after a while)
             {
                  outfile.WriteLine(JsonConvert.SerializeObject(msg)); 
             }

              queue = new List<Message>();
      }
}

我做错了什么?

【问题讨论】:

  • 我看不出有什么问题,但你为什么不简单地调用 queue.Clear() 而不是分配一个新的呢?这是我看到的唯一问题,即使我无法想象可能出了什么问题。
  • a:是否有任何其他操作与queue 通信,以及b:你为什么要重新分配queue
  • 我同意这两个 cmets,如果您在释放锁之前重新分配队列,这似乎是未定义的行为。
  • 哦,不。这是一个 O(n) 与一个 O(1)。我当然不希望这样,因为我不介意我已经迭代过的任何项目会发生什么。
  • @Brady 实际上是这样定义的:旧值已解锁

标签: c# multithreading list locking sync


【解决方案1】:

(下面;好吧,我想不出会导致这种情况的竞争条件......但是:谁知道......)

首先,您真的需要寻找与列表对话的其他代码;问题不在您发布的代码中。我怎么知道这个?因为在您枚举 (foreach (Message msg in queue)) 时,您在 queue 上有一个锁,而我们没有对锁对象的重新分配(非常狡猾,但不相关)做任何事情。

对于这个foreach 错误意味着其他东西正在改变列表。首先要做的事情很简单,就是重命名列表字段。如果其他代码触及列表,这将非常快速地向您显示。还要检查您是否从不公开此代码之外的列表,即从不从任何地方return queue;

问题似乎不在您显示的代码中。重新分配锁定对象是不好的做法,您不应该这样做 - 但是:我看不到实际破坏它的场景(显示代码)。


列表不是这里最好的模型,重新分配一个锁对象不是一个好主意。 要是有一个内置类型来表示队列就好了...

private readonly Queue<Message> queue = new Queue<Message>();
...
lock (queue) {
    queue.Enqueue(msg);
}

// 2nd function calls this
lock (queue) {
    if(queue.Count == 0) continue; // taken from comments

    using (StreamWriter outfile = new StreamWriter("path", true) {
        while(queue.Count != 0) {
            Message msg = queue.Dequeue();
            outfile.WriteLine(JsonConvert.SerializeObject(msg)); 
        }
    }
}

无需清除,因为 Dequeue 本质上有效地做到了这一点。

【讨论】:

  • 我发誓 - 除了我写的内容之外,我的代码中没有任何内容触及队列(你注意到评论了吗?)使用只读对象作为锁定标志解决了问题。
  • @gStation 好吧,我真的很惊讶。我自己想不出那场比赛,但是:线程总是很疯狂;p
  • 这很奇怪。这:“JsonConvert.SerializeObject”是一个外部工具,我不知道它是如何实现的。会不会是它做了一些内存管理,会危及 List 的安全?
  • 如果我可以使只读队列入队和出队,那么我显然不明白只读对集合的影响 - 需要解释一下吗?
  • @gStation readonly 只会阻止您将字段重新分配给 不同 队列 - 这会使您在任何时候都无法推断出什么对象被锁定;p跨度>
【解决方案2】:

lick 语句使用的参数应该是只读的。看到这个link

使用readonly private object 而不是queqe

代码应该是

eadonly object _object = new object();
// 1st function calls this
lock (_object)
{
      queue.Add(msg);
}

// 2nd function calls this
lock (_object)
{
      using (StreamWriter outfile = new StreamWriter("path", true)
      {
             foreach (Message msg in queue) // This is were I get the exception after a while)
             {
                  outfile.WriteLine(JsonConvert.SerializeObject(msg)); 
             }

              queue = new List<Message>();
      }
}

【讨论】:

  • 是的,应该是——我不反对:但是,你能说明一个比赛场景,它实际上在这里中断了吗?我不确定我能不能。我宁愿先亲自检查该列表的其他用途。
  • 之所以有效,是因为您没有锁定队列,而不是因为只读。请参阅@MarcGravell cmets。关于 Clear() 性能:如果它们是一个问题(在 JSON 序列化之后?)那么也许你甚至不会使用锁......
  • @Adriano 我知道这是因为锁定是在另一个对象上创建的(通过使用只读,您可以确保它没有被更改)。 json 序列化显然在性能上并不容易,但它目前对我来说是给定的。我希望有时间为此创建一个更好的日志机制。
  • @gStation 将只读对象设置为您没有设置的对象 constant (我错过了 C++ 的此功能),您只是阻止重新分配变量(您甚至可以声明您的只读列表)。此外,将锁与只读对象一起使用并不是强制性的(但这样做可以让您摆脱这种情况,只需用 Clear() 替换分配,您将无需任何其他更改即可解决)。关于性能的句子旨在作为对使用 Clear() 的评论:)
  • @gStation 只要队列很小,那么坦率地说,它是 O(1) 还是 O(n) 都没有关系。或者,不要使用列表来表示队列 - 使用队列
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-16
  • 1970-01-01
相关资源
最近更新 更多