"How to organize a thread-safe event call in C #, taking into account the fact that a large number of threads constantly subscribe to the event and unsubscribe from it?"
"How to organize a thread-safe event call in C #, taking into account the fact that a large number of threads constantly subscribe to the event and unsubscribe from it?"
public event Action MyLittleEvent; ... protected virtual void OnMyLittleEvent() { if (MyLittleEvent != null) MyLittleEvent(); }
OnMyLittleEvent
method OnMyLittleEvent
one thread can see that our MyLittleEvent
event MyLittleEvent
not null
, and another thread immediately after this check, but before calling the event, can remove its delegate from the list of subscribers, and thus make our event is MyLittleEvent null
, which will NullReferenceException
at the place where the event was NullReferenceException
.MyLittleEvent
field, while we still have a local reference to the old delegate object. protected virtual void OnMyLittleEvent() { Action tempAction = MyLittleEvent; // "" // tempAction , null if (tempAction != null) tempAction (); }
Volatile.Read(ref MyLittleEvent)
, that is: protected virtual void OnMyLittleEvent() { // Action tempAction = Volatile.Read(ref MyLittleEvent); if (tempAction != null) tempAction (); }
Volatile
separately for a long time, but in general it “just allows you to get rid of unwanted JIT compiler optimization”. On this occasion, there will be more clarifications and details, but for now we will dwell on the general idea of ​​the current decision of Jeffrey Richter:To ensure a thread-safe event call, you need to “freeze” the current list of subscribers by copying the event into a local variable, and then, if the resulting list is not empty, call all handlers from the frozen list. Thus we get rid of possibleNullReferenceException.
IDisposable
method, and follows a convention that determines that when trying to call any method on an object freed (hereinafter - disposed), it should throw an ObjectDisposedException
. Let's also agree that we unsubscribe from all events in the Dispose
method.Dispose
method on this object exactly after the moment when another thread “froze” its list of subscribers. The thread successfully calls the handler for the unsubscribing object, and during the attempt to process the event, the object sooner or later realizes that it has already been released, and throws an ObjectDisposedException
. Most likely, this exception is not caught in the handler itself, because it is logical to assume: “If our subscriber unsubscribed and was released, then his handler will never be called.” There will either crash the application, or the leak of uncontrolled resuses, or the event call will be interrupted when ObjectDisposedException
first appears (if we catch an exception when calling), but the event will not get to the normal "live" handlers.NullReferenceException
when invoking an event.It seems to me that all this hype with local variables is nothing more than Cargo Cult Programming . A large number of people solve the problem of thread-safe events in this way, while for full thread safety, much more needs to be done. I can say with confidence that those people who do not add such checks to their code can do without them. This problem simply does not exist in a single-threaded environment, and given that in the online examples with code you can rarely find the keywordvolatile
, this additional check may well be meaningless. If our task is to track aNullReferenceException
, is it not possible to do without checking fornull
at all by assigning an emptydelegate { }
our event during the initialization of the class object?
public event Action MyLittleEvent = delegate {};
MyLittleEvent
will never be null
, and you can simply not do an extra check. In a multithreaded environment, you only need to synchronize the addition and removal of event subscribers, but you can call it without fear of getting a NullReferenceException
: public event Action MyLittleEvent = delegate {}; protected virtual void OnMyLittleEvent() { // , MyLittleEvent(); }
add
, remove
and local “freeze” in lock
, which will get rid of possible uncertainties while simultaneously subscribing to the event of several threads: SomeEventHandler someEvent; readonly object someEventLock = new object(); public event SomeEventHandler SomeEvent { add { lock (someEventLock) { someEvent += value; } } remove { lock (someEventLock) { someEvent -= value; } } } protected virtual void OnSomeEvent(EventArgs e) { SomeEventHandler handler; lock (someEventLock) { handler = someEvent; } if (handler != null) { handler (this, e); } }
lock
, because this is very likely to result in a deadlock (hereafter, deadlock). In lock
only copying to a local variable is found; the event itself is called outside this structure.NullReferenceException
gives a very interesting thought:Yes, I really figured out this advice about trying to prevent aNullReferenceException
at all costs. I say that in our particular case, aNullReferenceException
can occur only if another thread unsubscribes from the event. And he does this only to never receive events again , which we, in fact, do not achieve when using checks of local variables. Where we hide the race condition we can open it and correct the consequences.NullReferenceException
allowsNullReferenceException
to determine the moment of mishandling your event. In general, I argue that this copying and verification technique is a simple Cargo-cult programming that adds confusion and noise to your code, but does not solve the problem of multi-threaded events at all.
The JIT compiler does not have the right to optimize the local reference to the delegate, because there is a condition. This information was “thrown in” some time ago, but this is not true (I clarified this question whether Joe Duffy or Vance Morrison). Without thevolatile
, the possibility simply arises that the local reference to the delegate will be a bit outdated, but in general this is all. This will not result in aNullReferenceException
.
And yes, we definitely have a race condition, you are right. But it will always be present. Suppose we remove the check fornull
and just writeMyLittleEvent();
Now imagine that our list of subscribers consists of 1000 delegates. It is possible that we will start to raise an event before one of the subscribers unsubscribes from it. In this case, it will still be called, since it will remain in the old list (do not forget that delegates are immutable). As I understand it, it is completely inevitable.
Using emptydelegate {};
it saves us from having to check the event fornull
, but this will not save us from the next race condition. Moreover, this method does not guarantee that we will use the most recent version of the event.
Volatile.Read
, since he further confirms the words of Skit:Although it is recommended to use the version withVolatile.Read
as the best and technically correct, you can get by with Option 2 , since the JIT compiler knows that it can accidentally mess up by optimizing the local variabletempAction
. Theoretically , in the future this may change, therefore it is recommended to use Option 3 . But in fact, Microsoft is unlikely to make such changes, because it can break a huge number of ready-made programs.
Volatile.Read
more equivalent. And no option will save from the race condition when calling unsubscribing handlers.NullReferenceException
it NullReferenceException
so much time and effort to prevent an unlikely NullReferenceException
, but not to prevent an equally probable call to the unsubscribing handler? That I did not understand. But in the process of searching for answers, I understood a lot of other things, and here is a small result.null
after checking for inequality. There is a danger of NullReferenceException
public event Action MyLittleEvent; ... protected virtual void OnMyLittleEvent() { if (MyLittleEvent != null) // NullReferenceException MyLittleEvent(); }
NullReferenceException
, but are not thread-safe , since the probability remains to call the already unsubscribing handlers. SomeEventHandler someEvent; readonly object someEventLock = new object(); public event SomeEventHandler SomeEvent { add { lock (someEventLock) { someEvent += value; } } remove { lock (someEventLock) { someEvent -= value; } } } protected virtual void OnSomeEvent(EventArgs e) { SomeEventHandler handler; lock (someEventLock) { handler = someEvent; } if (handler != null) { handler (this, e); } }
protected virtual void OnMyLittleEvent() { // Action tempAction = Volatile.Read(ref MyLittleEvent); if (tempAction != null) tempAction (); }
delegate {};
method delegate {};
allows you to get rid of NullReferenceException
due to the fact that the event never goes to null
, but is not thread-safe because the likelihood of invoking unsubscribing handlers remains. Moreover, without the volatile
, we have the opportunity to get not the latest version of the delegate when invoking an event.lock
, since this will create a deadlock hazard. Technically, it can save unsubscribing handlers from being called, but we cannot be sure about what actions the subscriber object did before unsubscribing from an event, so we can still run into a “damaged” object (see the example with ObjectDisposedException
). This method is also not thread safe .NullReferenceException
when NullReferenceException
events, there are no prescriptions about handlers. If you make a separate library, then most often you cannot influence its users in any way - you cannot force customers to assume that their handlers will not be called after the formal reply from the event.InvalidOperationException
as in the case of InvalidOperationException
changing collection through foreach
, but on the other, there is no way to check if someone has unsubscribed from the event during a call or not. The only thing that can be done by the event holder is to protect against a NullReferenceException
and hope that the subscribers will not spoil anything. As a result, the question can be answered as follows:It is impossible to provide a thread-safe event call in a multi-threaded environment, since there always remains the probability of calling the handlers of already unsubscribing subscribers. This uncertainty contradicts the definition of the term “thread safety”, in particular clauseIt is guaranteed that it can be used simultaneously.
Source: https://habr.com/ru/post/240385/
All Articles