首页 > 解决方案 > 收藏已修改;即使在锁定语句中专门修改了集合,枚举操作也可能不会执行

问题描述

我有以下基本代码。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() 在

WPFApplication.ActionMonitor.OnActionEnded()

看到这个堆栈跟踪时我的反应:这太不可思议了!这一定是一个 .Net 错误!

因为虽然ActionMonitor可以在多线程设置中使用,但上面的崩溃不应该发生——所有的_triggers(集合)修改都发生在一个lock语句中。这保证了一个人不能同时迭代修改集合。

而且,如果_triggers碰巧包含一个Actionthat ActionMonitor,那么我们可能会陷入僵局,但它永远不会崩溃。

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

我在这里想念什么吗?或者是否知道.Net 可以以一种非常古怪的方式表现它,当它涉及时System.Action

标签: c#multithreading

解决方案


您没有针对以下调用屏蔽您的代码:

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;
    }
}

推荐阅读