以下代码段来自Effective C#,
public event AddMessageEventHandler Log;
public void AddMsg ( int priority, string msg )
{
// This idiom discussed below.
AddMessageEventHandler l = Log;
if ( l != null )
l ( null, new LoggerEventArgs( priority, msg ) );
}
AddMsg方法显示了引发事件的正确方法。 引用日志事件处理程序的临时变量是防止竞争条件的重要保护措施 多线程程序。如果没有引用的副本,客户端可以删除if语句检查和事件处理程序执行之间的事件处理程序。通过 复制参考,这是不可能发生的。
为什么临时变量会阻止客户端删除事件处理程序?我必须在这里遗漏一些东西。
答案 0 :(得分:3)
它不会阻止客户端删除事件处理程序 - 它只是意味着你仍然会调用该事件处理程序。
您可能缺少的重要一点是委托是不可变的 - 当删除事件处理程序时,Log
的值将更改为新委托或null
。这没关系,因为在那个阶段你使用的是1
而不是Log
。
答案 1 :(得分:2)
委托链是不可变的。因此,如果另一个线程访问“Log”并删除了一个事件处理程序,则会为Log分配一个新的委托链。因此,当访问l时,即使从Log中删除了事件处理程序,它也不会影响l,因为它将不再“指向”同一个委托链。所以是的,它确实可以防止竞争条件,但是你最终会遇到一个线程取消订阅的情况,但仍会调用evanthandler。
答案 2 :(得分:2)
它不会阻止客户端删除事件处理程序 - 它只是意味着如果它们不会最终调用空委托...考虑:
Log
是否为空Log
变为空Log
(现在为null)= boom 当然,通过上述修复,您现在可以进行幻像调用 - 即订阅者可以在取消订阅之后调用事件...得到爱线程。
一如既往,Eric Lippert有一个关于这个主题的博客:Events and Races
答案 3 :(得分:1)
客户端仍然可以删除事件处理程序。
为:
if ( Log != null )
{
//another thread removes the event handler at this point,
// Log is now null
Log ( null, new LoggerEventArgs( priority, msg ) );
}
好:
AddMessageEventHandler l = Log;
if ( l != null )
//another thread removes the event handler at this point,
// Log is now null
// l is not null, so we are safe.
l ( null, new LoggerEventArgs( priority, msg ) );
答案 4 :(得分:0)
因为当你指定它时,你创建了一个对象的本地引用,然后在继续之前检查引用是否存在。
如果您刚刚检查了Log是否为null,则继续执行下一个语句,另一个线程可能会使2个指令之间的Log对象为空。
If(Log != null)
// another thread could null the reference to Log here
l.DoSomething()
AddMessageEventHandler l = Log;
if ( l != null )
// nothing can null the reference here as you’ve created a local copy of that reference
l ( null, new LoggerEventArgs( priority, msg ) );
}
答案 5 :(得分:0)
你加粗的文字解释了什么可能出错。
如果没有引用的副本,客户端可以删除if语句检查和事件处理程序执行之间的事件处理程序。
在AddMsg方法运行时,第二个线程可能会从Log中删除其事件处理程序。例如:
if (Log != null)
{
// other thread has just removed its event handler, so Log is now null
Log(null, new LoggerEventArgs(priority, msg)); // Oops! Throws NullReferenceException
}