C# 事件和线程安全
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/786383/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
C# Events and Thread Safety
提问by Daniel Earwicker
UPDATE
更新
As of C# 6, the answerto this question is:
从 C# 6 开始,这个问题的答案是:
SomeEvent?.Invoke(this, e);
I frequently hear/read the following advice:
我经常听到/阅读以下建议:
Always make a copy of an event before you check it for null
and fire it. This will eliminate a potential problem with threading where the event becomes null
at the location right between where you check for null and where you fire the event:
在检查null
并触发之前,请务必制作的副本。这将消除线程处理的潜在问题,其中null
位于检查 null 和触发的位置之间的位置:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
Updated: I thought from reading about optimizations that this might also require the event member to be volatile, but Jon Skeet states in his answer that the CLR doesn't optimize away the copy.
更新:我从阅读有关优化的文章中想到,这可能还要求成员具有可变性,但 Jon Skeet 在他的回答中指出,CLR 不会优化副本。
But meanwhile, in order for this issue to even occur, another thread must have done something like this:
但与此同时,为了使这个问题甚至发生,另一个线程必须这样做:
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
The actual sequence might be this mixture:
实际序列可能是这种混合物:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
The point being that OnTheEvent
runs after the author has unsubscribed, and yet they just unsubscribed specifically to avoid that happening. Surely what is really needed is a custom event implementation with appropriate synchronisation in the add
and remove
accessors. And in addition there is the problem of possible deadlocks if a lock is held while an event is fired.
关键是OnTheEvent
在作者取消订阅之后运行,但他们只是专门取消订阅以避免这种情况发生。当然,真正需要的是在add
和remove
访问器中具有适当同步的自定义实现。此外,如果在触发时持有锁,则可能会出现死锁问题。
So is this Cargo Cult Programming? It seems that way - a lot of people must be taking this step to protect their code from multiple threads, when in reality it seems to me that events require much more care than this before they can be used as part of a multi-threaded design. Consequently, people who are not taking that additional care might as well ignore this advice - it simply isn't an issue for single-threaded programs, and in fact, given the absence of volatile
in most online example code, the advice may be having no effect at all.
那么这是Cargo Cult 编程吗?似乎是这样 - 很多人必须采取这一步来保护他们的代码免受多线程的影响,而实际上在我看来,在它们可以用作多线程设计的一部分之前需要比这更加小心. 因此,那些没有特别注意的人可能会忽略这个建议——这对于单线程程序来说根本不是问题,事实上,鉴于volatile
大多数在线示例代码都没有,建议可能没有根本没有效果。
(And isn't it a lot simpler to just assign the empty delegate { }
on the member declaration so that you never need to check for null
in the first place?)
(并且delegate { }
在成员声明中分配空值这样您就不需要首先检查,这不是更简单null
吗?)
Updated:In case it wasn't clear, I did grasp the intention of the advice - to avoid a null reference exception under all circumstances. My point is that this particular null reference exception can only occur if another thread is delisting from the event, and the only reason for doing that is to ensure that no further calls will be received via that event, which clearly is NOT achieved by this technique. You'd be concealing a race condition - it would be better to reveal it! That null exception helps to detect an abuse of your component. If you want your component to be protected from abuse, you could follow the example of WPF - store the thread ID in your constructor and then throw an exception if another thread tries to interact directly with your component. Or else implement a truly thread-safe component (not an easy task).
更新:如果不清楚,我确实掌握了建议的意图 - 在所有情况下都避免空引用异常。我的观点是,这个特殊的空引用异常只有在另一个线程从中删除时才会发生,这样做的唯一原因是确保不会通过该接收到进一步的调用,这显然不是通过这种技术实现的. 你会隐瞒比赛条件 - 最好揭露它!该空异常有助于检测对组件的滥用。如果您希望您的组件免受滥用,您可以按照 WPF 的示例 - 将线程 ID 存储在您的构造函数中,然后在另一个线程尝试直接与您的组件交互时抛出异常。或者实现一个真正的线程安全组件(不是一件容易的事)。
So I contend that merely doing this copy/check idiom is cargo cult programming, adding mess and noise to your code. To actually protect against other threads requires a lot more work.
所以我认为,仅仅执行这种复制/检查习语是对货物崇拜的编程,给你的代码添加混乱和噪音。要真正防止其他线程需要做更多的工作。
Update in response to Eric Lippert's blog posts:
更新响应 Eric Lippert 的博客文章:
So there's a major thing I'd missed about event handlers: "event handlers are required to be robust in the face of being called even after the event has been unsubscribed", and obviously therefore we only need to care about the possibility of the event delegate being null
. Is that requirement on event handlers documented anywhere?
所以我错过了关于处理程序的一个主要事情:“即使在取消订阅之后,处理程序也必须在被调用时保持健壮”,因此显然我们只需要关心的可能性委托是null
. 是否在任何地方记录了对处理程序的要求?
And so: "There are other ways to solve this problem; for example, initializing the handler to have an empty action that is never removed. But doing a null check is the standard pattern."
所以:“还有其他方可以解决这个问题;例如,将处理程序初始化为一个永远不会被删除的空操作。但进行空检查是标准模式。”
So the one remaining fragment of my question is, why is explicit-null-check the "standard pattern"?The alternative, assigning the empty delegate, requires only = delegate {}
to be added to the event declaration, and this eliminates those little piles of stinky ceremony from every place where the event is raised. It would be easy to make sure that the empty delegate is cheap to instantiate. Or am I still missing something?
所以我的问题剩下的一个片段是,为什么显式空检查是“标准模式”?另一种方是分配空委托,只需= delegate {}
要将其添加到声明中,这样就消除了从每个引发的地方的小堆臭味仪式。很容易确保实例化空委托的成本很低。或者我还缺少什么?
Surely it must be that (as Jon Skeet suggested) this is just .NET 1.x advice that hasn't died out, as it should have done in 2005?
肯定是这样(正如 Jon Skeet 所建议的)这只是 .NET 1.x 建议并没有消失,就像它在 2005 年应该做的那样?
采纳答案by Jon Skeet
The JIT isn't allowed to perform the optimization you're talking about in the first part, because of the condition. I know this was raised as a spectre a while ago, but it's not valid. (I checked it with either Joe Duffy or Vance Morrison a while ago; I can't remember which.)
由于条件的原因,不允许 JIT 执行您在第一部分中讨论的优化。我知道这是不久前作为幽灵提出的,但这是无效的。(我不久前与乔达菲或万斯莫里森核对过;我不记得是哪个了。)
Without the volatile modifier it's possible that the local copy taken will be out of date, but that's all. It won't cause a NullReferenceException
.
如果没有 volatile 修饰符,所获取的本地副本可能会过时,但仅此而已。它不会导致NullReferenceException
.
And yes, there's certainly a race condition - but there always will be. Suppose we just change the code to:
是的,肯定存在竞争条件——但总会有的。假设我们只是将代码更改为:
TheEvent(this, EventArgs.Empty);
Now suppose that the invocation list for that delegate has 1000 entries. It's perfectly possible that the action at the start of the list will have executed before another thread unsubscribes a handler near the end of the list. However, that handler will still be executed because it'll be a new list. (Delegates are immutable.) As far as I can see this is unavoidable.
现在假设该委托的调用列表有 1000 个条目。完全有可能在另一个线程取消订阅列表末尾附近的处理程序之前,列表开头的操作已经执行。但是,该处理程序仍将被执行,因为它将是一个新列表。(代表是不可变的。)据我所知,这是不可避免的。
Using an empty delegate certainly avoids the nullity check, but doesn't fix the race condition. It also doesn't guarantee that you always "see" the latest value of the variable.
使用空委托当然可以避免无效检查,但不能解决竞争条件。它也不保证您总是“看到”变量的最新值。
回答by Jason Coyne
for single threaded applicaitons, you are correc this is not an issue.
对于单线程应用程序,您是正确的,这不是问题。
However, if you are making a component that exposes events, there is no guarantee that a consumer of your component is not going to go multithreading, in which case you need to prepare for the worst.
但是,如果您正在制作一个公开的组件,则无保证您的组件的使用者不会使用多线程,在这种情况下,您需要为最坏的情况做好准备。
Using the empty delegate does solve the problem, but also causes a performance hit on every call to the event, and could possibly have GC implications.
使用空委托确实解决了问题,但也会导致每次调用时性能下降,并且可能会产生 GC 影响。
You are right that the consumer trie dto unsubscribe in order for this to happen, but if they made it past the temp copy, then consider the message already in transit.
您是正确的,消费者尝试取消订阅以便发生这种情况,但是如果他们通过临时副本,那么请考虑消息已经在传输中。
If you don't use the temporary variable, and don't use the empty delegate, and someone unsubscribes, you get a null reference exception, which is fatal, so I think the cost is worth it.
如果不使用临时变量,不使用空委托,有人退订,就会得到空引用异常,这是致命的,所以我认为付出的代价是值得的。
回答by JP Alioto
I see a lot of people going toward the extension method of doing this ...
我看到很多人都朝着这样做的扩展方......
public static class Extensions
{
public static void Raise<T>(this EventHandler<T> handler,
object sender, T args) where T : EventArgs
{
if (handler != null) handler(sender, args);
}
}
That gives you nicer syntax to raise the event ...
这为您提供了更好的语来引发......
MyEvent.Raise( this, new MyEventArgs() );
And also does away with the local copy since it is captured at method call time.
并且还取消了本地副本,因为它是在方调用时捕获的。
回答by dss539
This practice is not about enforcing a certain order of operations. It's actually about avoiding a null reference exception.
这种做不是强制执行特定的操作顺序。它实际上是关于避免空引用异常。
The reasoning behind people caring about the null reference exception and not the race condition would require some deep psychological research. I think it has something to do with the fact that fixing the null reference problem is much easier. Once that is fixed, they hang a big "Mission Accomplished" banner on their code and unzip their flight suit.
人们关心空引用异常而不是竞争条件背后的推理需要一些深入的心理学研究。我认为这与修复空引用问题要容易得多的事实有关。一旦解决了这个问题,他们就会在他们的代码上挂上一个巨大的“任务完成”横幅并解开他们的飞行服。
Note: fixing the race condition probably involves using a synchronous flag track whether the handler should run
注意:修复竞争条件可能涉及使用同步标志跟踪处理程序是否应该运行
回答by Greg D
I've never really considered this to be much of an issue because I generally only protect against this sort of potential threading badness in static methods (etc) on my reusable components, and I don't make static events.
我从来没有真正认为这是一个很大的问题,因为我通常只在可重用组件的静态方(等)中防止这种潜在的线程不良,并且我不制作静态。
Am I doing it wrong?
我做错了吗?
回答by Levi
So I'm a little late to the party here. :)
所以我来这里参加派对有点晚了。:)
As for the use of null rather than the null object pattern to represent events with no subscribers, consider this scenario. You need to invoke an event, but constructing the object (EventArgs) is non-trivial, and in the common case your event has no subscribers. It would be beneficial to you if you could optimize your code to check to see if you had any subscribers at all before you committed processing effort to constructing the arguments and invoking the event.
至于使用 null 而不是 null 对象模式来表示没有订阅者的,请考虑这种情况。您需要调用一个,但构造对象 (EventArgs) 并非易事,而且在通常情况下,您的没有订阅者。如果您可以优化您的代码以在您致力于构建参数和调用之前检查您是否有任何订阅者,那将对您有益。
With this in mind, a solution is to say "well, zero subscribers is represented by null." Then simply perform the null check before performing your expensive operation. I suppose another way of doing this would have been to have a Count property on the Delegate type, so you'd only perform the expensive operation if myDelegate.Count > 0. Using a Count property is a somewhat nice pattern that solves the original problem of allowing optimization, and it also has the nice property of being able to be invoked without causing a NullReferenceException.
考虑到这一点,解决方案是说“好吧,零订阅者由 null 表示”。然后在执行昂贵的操作之前简单地执行空检查。我想另一种方是在 Delegate 类型上有一个 Count 属性,所以如果 myDelegate.Count > 0,你只会执行昂贵的操作。使用 Count 属性是一个很好的模式,可以解决原始问题允许优化,并且它还有一个很好的特性,即可以在不导致 NullReferenceException 的情况下被调用。
Keep in mind, though, that since delegates are reference types, they are allowed to be null. Perhaps there was simply no good way of hiding this fact under the covers and supporting only the null object pattern for events, so the alternative may have been forcing developers to check both for null and for zero subscribers. That would be even uglier than the current situation.
但是请记住,由于委托是引用类型,因此它们可以为 null。也许根本就没有什么好方可以将这个事实隐藏在幕后,只支持的空对象模式,因此替代方案可能是迫使开发人员同时检查空订阅者和零订阅者。那会比现在的情况还要丑陋。
Note: This is pure speculation. I'm not involved with the .NET languages or CLR.
注:纯属猜测。我不参与 .NET 语言或 CLR。
回答by Daniel Fortunov
"Why is explicit-null-check the 'standard pattern'?"
“为什么显式空检查是‘标准模式’?”
I suspect the reason for this might be that the null-check is more performant.
我怀疑这样做的原因可能是空检查的性能更高。
If you always subscribe an empty delegate to your events when they are created, there will be some overheads:
如果你总是在创建时订阅一个空委托,将会有一些开销:
- Cost of constructing the empty delegate.
- Cost of constructing a delegate chain to contain it.
- Cost of invoking the pointless delegate every single time the event is raised.
- 构建空委托的成本。
- 构建一个包含它的委托链的成本。
- 每次引发时调用无意义委托的成本。
(Note that UI controls often have a large number of events, most of which are never subscribed to. Having to create a dummy subscriber to each event and then invoke it would likely be a significant performance hit.)
(请注意,UI 控件通常有大量,其中大部分从未被订阅。必须为每个创建一个虚拟订阅者,然后调用它可能会严重影响性能。)
I did some cursory performance testing to see the impact of the subscribe-empty-delegate approach, and here are my results:
我做了一些粗略的性能测试,以查看 subscribe-empty-delegate 方的影响,以下是我的结果:
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took: 432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took: 614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took: 674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took: 2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took: 2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took: 2246ms <--
Done
Note that for the case of zero or one subscribers (common for UI controls, where events are plentiful), the event pre-initialised with an empty delegate is notably slower (over 50 million iterations...)
请注意,对于零个或一个订阅者的情况(对于 UI 控件很常见,其中很多),使用空委托预初始化的明显变慢(超过 5000 万次迭代......)
For more information and source code, visit this blog post on .NET Event invocation thread safetythat I published just the day before this question was asked (!)
有关更多信息和源代码,请访问我在提出此问题的前一天发布的有关.NET 调用线程安全的博客文章(!)
(My test set-up may be flawed so feel free to download the source code and inspect it yourself. Any feedback is much appreciated.)
(我的测试设置可能有缺陷,因此请随时下载源代码并自行检查。非常感谢任何反馈。)
回答by Triynko
Wire all your events at construction and leave them alone. The design of the Delegate class cannot possibly handle any other usage correctly, as I will explain in the final paragraph of this post.
在施工时连接您的所有活动,让它们独自一人。Delegate 类的设计不可能正确处理任何其他用,我将在本文的最后一段解释。
First of all, there's no point in trying to interceptan event notificationwhen your event handlersmust already make a synchronized decision about whether/how to respond to the notification.
首先,当您的处理程序必须已经就是否/如何响应通知做出同步决定时,尝试拦截通知是没有意义的。
Anything that may be notified, should be notified. If your event handlers are properly handling the notifications (i.e. they have access to an authoritative application state and respond only when appropriate), then it will be fine to notify them at any time and trust they will respond properly.
任何可能被通知的东西,都应该被通知。如果您的处理程序正确处理通知(即他们有权访问权威的应用程序状态并仅在适当时响应),那么随时通知他们并相信他们会正确响应就可以了。
The only time a handler shouldn't be notified that an event has occurred, is if the event in fact hasn't occurred! So if you don't want a handler to be notified, stop generating the events (i.e. disable the control or whatever is responsible for detecting and bringing the event into existence in the first place).
唯一不应该通知处理程序发生了的情况是,如果该实际上还没有发生!因此,如果您不希望通知处理程序,请停止生成(即首先禁用控件或负责检测和使存在的任何内容)。
Honestly, I think the Delegate class is unsalvageable. The merger/transition to a MulticastDelegate was a huge mistake, because it effectively changed the (useful) definition of an event from something that happens at a single instant in time, to something that happens over a timespan. Such a change requires a synchronization mechanism that can logically collapse it back into a single instant, but the MulticastDelegate lacks any such mechanism. Synchronization should encompass the entire timespan or instant the event takes place, so that once an application makes the synchronized decision to begin handling an event, it finishes handling it completely (transactionally). With the black box that is the MulticastDelegate/Delegate hybrid class, this is near impossible, so adhere to using a single-subscriber and/or implement your own kind of MulticastDelegate that has a synchronization handle that can be taken out while the handler chain is being used/modified. I'm recommending this, because the alternative would be to implement synchronization/transactional-integrity redundantly in all your handlers, which would be ridiculously/unnecessarily complex.
老实说,我认为 Delegate 类是无挽救的。合并/转换到 MulticastDelegate 是一个巨大的错误,因为它有效地将的(有用的)定义从某个瞬间发生的事情更改为在一段时间内发生的事情。这种更改需要一种同步机制,可以在逻辑上将其折叠回单个瞬间,但 MulticastDelegate 缺乏任何此类机制。同步应该涵盖整个时间跨度或发生的瞬间,以便一旦应用程序做出开始处理的同步决定,它就完全(事务性地)完成处理。使用 MulticastDelegate/Delegate 混合类的黑盒,这几乎是不可能的,所以坚持使用单订阅者和/或实现您自己的 MulticastDelegate 类型,它具有同步句柄,可以在使用/修改处理程序链时取出。我推荐这个,因为另一种方是在所有处理程序中冗余地实现同步/事务完整性,这将是荒谬的/不必要的复杂。
回答by alyx
According to Jeffrey Richter in the book CLR via C#, the correct method is:
根据 Jeffrey Richter 在CLR via C#一书中的说,正确的方是:
// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);
Because it forces a reference copy. For more information, see his Event section in the book.
因为它强制引用副本。有关更多信息,请参阅本书中他的部分。
回答by Chuck Savage
I truly enjoyed this read - not! Even though I need it to work with the C# feature called events!
我真的很喜欢这本书——不是!即使我需要它来使用称为的 C# 功能!
Why not fix this in the compiler? I know there are MS people who read these posts, so please don't flame this!
为什么不在编译器中解决这个问题?我知道有 MS 人读过这些帖子,所以请不要喷这个!
1 - the Null issue) Why not make events be .Empty instead of null in the first place? How many lines of code would be saved for null check or having to stick a = delegate {}
onto the declaration? Let the compiler handle the Empty case, IE do nothing! If it all matters to the creator of the event, they can check for .Empty and do whatever they care with it! Otherwise all the null checks / delegate adds are hacks around the problem!
1 - Null 问题)为什么不首先将设为 .Empty 而不是 null?为空检查或必须将 a= delegate {}
粘贴到声明上将保存多少行代码?让编译器处理Empty case,IE什么都不做!如果这一切对的创建者都很重要,他们可以检查 .Empty 并做任何他们关心的事情!否则所有空检查/委托添加都是解决问题的技巧!
Honestly I'm tired of having to do this with every event - aka boilerplate code!
老实说,我厌倦了在每个中都必须这样做 - 也就是样板代码!
public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
var e = Some; // avoid race condition here!
if(null != e) // avoid null condition here!
e(this, someValue);
}
2 - the race condition issue) I read Eric's blog post, I agree that the H (handler) should handle when it dereferences itself, but cannot the event be made immutable/thread safe? IE, set a lock flag on its creation, so that whenever it is called, it locks all subscribing and un-subscribing to it while its executing?
2 - 竞争条件问题)我阅读了 Eric 的博客文章,我同意 H(处理程序)在取消引用自身时应该处理,但不能使不可变/线程安全吗?IE,在其创建时设置一个锁定标志,以便无论何时调用它,它都会在执行时锁定所有订阅和取消订阅?
Conclusion,
结论,
Are not modern day languages supposed to solve problems like these for us?
现代语言不应该为我们解决这些问题吗?