【问题标题】:Using locks doesn't prevent Collection was modified; enumeration operation may not execute使用锁不会阻止 Collection 被修改;枚举操作可能无法执行
【发布时间】:2016-11-07 09:12:54
【问题描述】:

这个问题已经被问过很多次了,但这是一个特例。

public class JobStatusMonitor
{
    private static List<Job> _runningJobs = new List<Job>();
    private static object myLock = new object();

    public static void AddJob(GPSJob input)
    {
        lock (myLock)
            _runningJobs.Add(input);
    }

    public static void Start(int pollInterval)
    {
        while (true)
        {
            var removeJobs = new List<GPSJob>();
            lock (myLock)
            {
                foreach (var job in _runningJobs)
                {
                    if (job.IsComplete())
                    {
                        removeJobs.Add(job);
                    }
                }
            }

            foreach (var job in removeJobs)
            {
                _runningJobs.Remove(job);
            }

            System.Threading.Thread.Sleep(pollInterval);
        }
    }
}

列表 _runningJobs 是私有的,因此除非它使用 AddJob 方法,否则此类之外的任何内容都不能修改它。 AddJob 方法使用与 foreach 循环相同的锁,因此它应该无法在迭代时修改集合。

我对应该发生的事情的理解是 Start(5000) 被调用,列表中没有任何内容,因此它跳到 Thread.Sleep()。后台进程将作业添加到列表中。 while 循环返回到 foreach 循环并应用锁。在迭代列表时,任何其他尝试添加到集合的线程都会等待迭代完成。一旦迭代完成,这些线程中的每一个都会添加它们的作业,即使有许多线程试图添加作业,锁也不会导致竞争条件。

实际发生的情况是,在此线程休眠时添加的任何作业都已成功添加。尽管有锁定,但在此列表迭代时添加的作业不会等待迭代完成。

为什么锁不能防止这个错误?

编辑:复制到锁内的新列表可消除错误。

public class JobStatusMonitor
{
    private static List<Job> _runningJobs = new List<Job>();
    private static object myLock = new object();

    public static void AddJob(GPSJob input)
    {
        lock (myLock)
        {
           _runningJobs.Add(input);
        }
    }

    public static void Start(int pollInterval)
    {
        while (true)
        {

            lock (myLock)
            {
                var completeJobs = _runningJobs.Where(job => job.IsComplete()).ToList();
                foreach (var job in completeJobs)
                {
                    _runningJobs.Remove(job);
                    job.TaskCompletionSource.SetResult(null);
                }       
            }

            System.Threading.Thread.Sleep(pollInterval);
        }
    }
}

【问题讨论】:

  • 请注意,您的最终foreach 正在修改锁之外的集合。
  • 如何知道在迭代过程中添加了作业?你有这方面的证据吗?为什么 Remove() 在锁之外?
  • 您似乎正在使用列表来模拟队列。为什么不在while 循环中使用ConcurrentQueue已经线程安全),而不是在其他线程尝试向其中添加项目时尝试枚举列表并阻塞?跨度>
  • @DStanley 他可以删除列表中间的工作,如果他们在其他人之前完成。
  • 你是从不同的线程调用Start还是只调用一次?

标签: c# multithreading exception


【解决方案1】:

@Philipe 发现了问题,您正在修改锁之外的列表。您应该让所有修改调用都受到锁的保护。

为简化起见,您可以只计算未完成作业的新列表并与锁内的当前作业列表交换。大致如下:

lock(myLock) {
    var newRunningJobs = _runningJob.Where(j => !Job.IsComplete(j)).ToList();
    _runningJob = newRunningJobs;
}

【讨论】:

  • 使用基于此解决方案的反面的解决方案编辑问题。除了将它们从列表中删除之外,我还需要处理已完成的作业,但为简单起见,此示例未包含在此示例中。
  • 创建集合的新实例(引用)。不确定这是可取的。
  • 我想这是安全的,因为它是一个私人成员
  • 这就是它起作用的原因。公共方法更新源列表,并在新副本上执行迭代(直到下一个周期才会更新),因此被迭代的集合在迭代期间不会被修改。由于某些未知原因,该锁适用于 ToList,但不适用于 add + foreach。
  • @fjardon 我遇到了类似的情况,但我使用了 Property(of the list) 而不是 List 本身,所有修改和读取都是通过这个属性以及 Set 和 Get 完成的我已经锁定的部分财产。您能告诉我myLock 对象锁定和该列表本身锁定之间的区别吗?
【解决方案2】:

您对_runningJobs.Remove(job); 的呼叫未锁定。该集合无法意识到它,并且您正在从中删除项目,同时可能在AddJob 中锁定它。将移除的工作锁在一个锁中或将其包裹起来,它应该可以解决您的问题。

【讨论】:

  • 我把除了thread.Sleep()之外的所有东西都移到了锁里,得到了同样的错误
  • @Adam 你遇到了什么错误?你能准确描述什么没有按预期工作吗?
  • 我认为他不应该编辑这个问题。他测试了这个答案并说它不起作用。答案对我来说似乎很好,并希望他解释什么不起作用,但原来的问题是存在的。
  • 第二个循环移入锁时,错误没有改变。与原始问题中描述的相同位置的错误相同。
猜你喜欢
  • 2011-09-04
  • 2016-06-06
  • 1970-01-01
  • 2021-02-25
  • 1970-01-01
  • 2012-04-13
  • 1970-01-01
相关资源
最近更新 更多