C#事件和线程安全 [英] C# Events and Thread Safety

查看:91
本文介绍了C#事件和线程安全的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我经常听到/阅读以下建议:

I frequently hear/read the following advice:

在您检查 null 并点燃它。这将消除线程之间的潜在问题,其中事件变为 null 在您检查null和您触发事件的位置之间的位置:

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:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

更新:我以为阅读关于优化这可能还需要事件成员是不稳定的优化,但Jon Skeet在他的回答中指出,CLR没有优化副本。

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.

但同时为了这个问题甚至发生,另一个线程必须这样做:

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

这点就是 OnTheEvent 运行后,作者已取消订阅,但他们只是取消订阅,以避免发生这种情况。真正需要的是在 add 删除访问器中进行适当同步的自定义事件实现。另外还有一个可能的死锁问题,如果一个事件被触发时被锁定。

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.

所以这是?似乎这样 - 很多人必须采取这一步来保护他们的代码免受多线程的影响,实际上在我看来,事件需要比这更多的关心,才能被用作多线程设计的一部分。因此,没有采取额外护理的人也可能忽略这一建议 - 这对单线程程序来说不是一个问题,实际上由于缺少 volatile 在大多数在线示例代码中,建议可能根本没有任何效果。

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.

(而且,分配空的$ $ c并不简单$ c> delegate {} 在成员声明中,以便您首先不需要检查 null

(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?)

更新:如果不清楚,我确实抓住了建议的意图,以避免在任何情况下引用空引用异常。我的观点是,这个特定的空引用异常只能在另一个线程从事件中退出时发生,唯一的原因是确保不会通过该事件接收到进一步的调用,这显然不能通过这种技术来实现。你会隐藏一个种族条件 - 最好是揭示它!该空异常有助于检测您的组件的滥用。如果您希望您的组件受到保护以免受到滥用,您可以按照WPF的示例 - 将线程ID存储在构造函数中,然后如果另一个线程尝试直接与组件交互,则抛出异常。或者实现一个真正的线程安全的组件(不是一件容易的任务)。

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).

所以我认为只是做这个复制/检查成语是货物崇拜编程,增加了混乱噪音对你的代码。要实际保护其他线程需要更多的工作。

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.

更新以回应Eric Lippert的博文:

所以我有一件很重要的事情,我错过了事件处理程序:即使事件被取消订阅之后,事件处理程序也需要保持强大,因此我们只需要关心事件委托的可能性是 null

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?

所以:还有其他方法可以解决这个问题;例如,初始化处理程序有一个空的动作永远不会被删除,但做一个空的检查是标准模式。

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."

所以我的问题的剩下的一部分是,为什么是显式空 - 检查标准模式?另外,分配空委托只需要将 = delegate {} 添加到事件声明中,这消除了从事件发生的每一个地方那些一堆臭的仪式。这将很容易确保空委托人便宜实例化。或者我仍然缺少某些东西?

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?

肯定是(正如Jon Skeet所建议的),这只是.NET 1.x的建议,没有消失,因为它应该在2005年完成?

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?

推荐答案

JIT不允许在第一个部分,因为条件。我知道这是一个幽灵在一段时间之前提出的,但是这是无效的。 (我刚才和Joe Duffy或者Vance Morrison一起检查过;我不记得哪一个了。)

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.)

如果没有volatile修饰符,那么本地拷贝可能会已经过时了,但这就是全部。它不会导致 NullReferenceException

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.

是的,肯定有一个竞争条件 - 但总是会有。假设我们只是将代码更改为:

And yes, there's certainly a race condition - but there always will be. Suppose we just change the code to:

TheEvent(this, EventArgs.Empty);

现在假设该委托的调用列表有1000个条目。完全可能的是,列表开头的操作将在另一个线程取消订阅列表末尾附近的处理程序之前执行。但是,该处理程序仍将被执行,因为它将是一个新的列表。 (代表是不可变的)。据我所见,这是不可避免的。

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.

使用空委托者肯定会避免无效检查,但不能修复竞争条件。它也不能保证您始终看到变量的最新值。

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.

这篇关于C#事件和线程安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆