集合已修改;即使仅在锁语句中修改了集合,枚举操作也可能不会执行

时间:2018-09-21 13:02:03

标签: c# multithreading

我有以下基本代码。 ActionMonitor可以由任何人在任何设置下使用,无论单线程还是多线程。

using System;
public class ActionMonitor
{
    public ActionMonitor()
    {

    }


    private object _lockObj = new object();

    public void OnActionEnded()
    {
        lock (_lockObj)
        {
            IsInAction = false;
            foreach (var trigger in _triggers)
                trigger();
            _triggers.Clear();
        }
    }

    public void OnActionStarted()
    {
        IsInAction = true;
    }

    private ISet<Action> _triggers = new HashSet<Action>();
    public void ExecuteAfterAction(Action action)
    {
        lock (_lockObj)
        {
            if (IsInAction)
                _triggers.Add(action);
            else
                action();
        }
    }

    public bool IsInAction
    {
       get;private set;

    }
}

有一次,当我检查客户端计算机崩溃时,在以下位置抛出异常:

  

System.Core:System.InvalidOperationException集合已修改;枚举操作可能无法执行。在

     

System.Collections.Generic.HashSet`1.Enumerator.MoveNext()at

     

WPFApplication.ActionMonitor.OnActionEnded()

我看到此堆栈跟踪时的反应:这真是难以置信!这必须是.NET错误!。

因为尽管ActionMonitor可以在多线程设置中使用,但是上面的崩溃不应该发生-所有_triggers(集合)的修改都发生在lock语句中。这样可以保证一个人不能遍历集合 并同时对其进行修改。

并且,如果_triggers恰好包含一个涉及Action的{​​{1}},那么我们可能会陷入僵局,但永远不会崩溃。

我只见过一次崩溃,所以根本无法重现该问题。但是基于我对多线程和ActionMonitor语句的理解,永远不会发生这种异常。

我在这里想念什么吗?还是知道.Net在涉及lock时可以以一种非常古怪的方式表现出来?

2 个答案:

答案 0 :(得分:1)

您并未针对以下调用屏蔽代码:

private static ActionMonitor _actionMonitor;

static void Main(string[] args)
{
    _actionMonitor = new ActionMonitor();
    _actionMonitor.OnActionStarted();
    _actionMonitor.ExecuteAfterAction(Foo1);
    _actionMonitor.ExecuteAfterAction(Foo2);
    _actionMonitor.OnActionEnded();

    Console.ReadLine();
}
private static void Foo1()
{
    _actionMonitor.OnActionStarted();
    //Notice that if you would call _actionMonitor.OnActionEnded(); here instead of _actionMonitor.OnActionStarted(); - you would get a StackOverflow Exception
    _actionMonitor.ExecuteAfterAction(Foo3);
}
private static void Foo2()
{

}
private static void Foo3()
{

}

仅供参考-评论中正在讨论的场景Damien_The_Unbeliever

要解决此问题,仅想到的两件事是

  1. 不要这样称呼它,这是您的类,并且您的代码正在调用它,因此请确保您遵守自己的规则

  2. 获取_trigger列表的副本并对其进行枚举

关于第1点,您可以跟踪OnActionEnded是否正在运行,如果运行时调用了OnActionStarted,则会引发异常:

private bool _isRunning = false;
public void OnActionEnded()
{
    lock (_lockObj)
    {
        try
        {
            _isRunning = true;

            IsInAction = false;
            foreach (var trigger in _triggers)
                trigger();
            _triggers.Clear();
        }
        finally
        {
            _isRunning = false;
        }
    }
}

public void OnActionStarted()
{
    lock (_lockObj)
    {        
        if (_isRunning)
            throw new NotSupportedException();

        IsInAction = true;
    }
}

关于第2点,情况如何

public class ActionMonitor
{
    public ActionMonitor()
    {

    }

    private object _lockObj = new object();

    public void OnActionEnded()
    {
        lock (_lockObj)
        {
            IsInAction = false;

            var tmpTriggers = _triggers;
            _triggers = new HashSet<Action>();
            foreach (var trigger in tmpTriggers)
                trigger();

            //have to decide what to do if _triggers isn't empty here, we could use a while loop till its empty
            //so for example

            while (true)
            {    
                var tmpTriggers = _triggers;
                _triggers = new HashSet<Action>();
                if (tmpTriggers.Count == 0)
                    break;

                foreach (var trigger in tmpTriggers)
                    trigger();
            }
        }
    }

    public void OnActionStarted()
    {
        lock (_lockObj) //fix the error @EricLippert talked about in comments
            IsInAction = true;
    }

    private ISet<Action> _triggers = new HashSet<Action>();
    public void ExecuteAfterAction(Action action)
    {
        lock (_lockObj)
        {
            if (IsInAction)
                _triggers.Add(action);
            else
                action();
        }
    }

    public bool IsInAction
    {
       get;private set;
    }
}

答案 1 :(得分:0)

  

这保证了不能迭代集合并同时修改它。

不。您遇到了重入问题。

考虑如果内部trigger的调用(相同的线程,因此已经持有锁)会发生什么,您修改集合:

csharp foreach (var trigger in _triggers) trigger(); // _triggers modified in here

实际上,如果您查看整个调用堆栈,便可以找到枚举集合的框架。(到发生异常时,修改集合的代码已经被删除。)从堆栈中弹出)