使用锁不会阻止Collection被修改;枚举操作可能无法执行

Ada*_*dam 2 c# multithreading exception

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

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

列表_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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

fja*_*don 5

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

为简化起见,您可以只计算新的未完成作业列表,然后与锁中的当前作业列表交换。类似于以下内容:

lock(myLock) {
    var newRunningJobs = _runningJob.Where(j => !Job.IsComplete(j)).ToList();
    _runningJob = newRunningJobs;
}
Run Code Online (Sandbox Code Playgroud)