C# Events and Thread Safety

前端 未结 15 1070
甜味超标
甜味超标 2020-11-22 06:00

UPDATE

As of C# 6, the answer to this question is:

SomeEvent?.Invoke(this, e);

I frequently hear/read the fo

相关标签:
15条回答
  • 2020-11-22 06:03

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

    I did some cursory performance testing to see the impact of the subscribe-empty-delegate approach, and here are my results:

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

    For more information and source code, visit this blog post on .NET Event invocation thread safety that I published just the day before this question was asked (!)

    (My test set-up may be flawed so feel free to download the source code and inspect it yourself. Any feedback is much appreciated.)

    0 讨论(0)
  • 2020-11-22 06:06

    I don't believe the question is constrained to the c# "event" type. Removing that restriction, why not re-invent the wheel a bit and do something along these lines?

    Raise event thread safely - best practice

    • Ability to sub/unsubscribe from any thread while within a raise (race condition removed)
    • Operator overloads for += and -= at the class level.
    • Generic caller-defined delegate
    0 讨论(0)
  • 2020-11-22 06:08

    With C# 6 and above, code could be simplified using new ?. operator as in:

    TheEvent?.Invoke(this, EventArgs.Empty);

    Here is the MSDN documentation.

    0 讨论(0)
  • 2020-11-22 06:08

    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.

    First of all, there's no point in trying to intercept an event notification when your event handlers must 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.

    0 讨论(0)
  • 2020-11-22 06:10

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

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

    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.

    0 讨论(0)
  • 2020-11-22 06:11

    According to Jeffrey Richter in the book CLR via C#, the correct method is:

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

    0 讨论(0)
提交回复
热议问题