我有以下基本代码。 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
时可以以一种非常古怪的方式表现出来?
答案 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
。
要解决此问题,仅想到的两件事是
不要这样称呼它,这是您的类,并且您的代码正在调用它,因此请确保您遵守自己的规则
获取_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
实际上,如果您查看整个调用堆栈,便可以找到枚举集合的框架。(到发生异常时,修改集合的代码已经被删除。)从堆栈中弹出)