C# Events and Thread Safety

前端 未结 15 1124
甜味超标
甜味超标 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:11

    I've been using this design pattern to ensure that event handlers aren't executed after they're unsubscribed. It's working pretty well so far, although I haven't tried any performance profiling.

    private readonly object eventMutex = new object();
    
    private event EventHandler _onEvent = null;
    
    public event EventHandler OnEvent
    {
      add
      {
        lock(eventMutex)
        {
          _onEvent += value;
        }
      }
    
      remove
      {
        lock(eventMutex)
        {
          _onEvent -= value;
        }
      }
    
    }
    
    private void HandleEvent(EventArgs args)
    {
      lock(eventMutex)
      {
        if (_onEvent != null)
          _onEvent(args);
      }
    }
    

    I'm mostly working with Mono for Android these days, and Android doesn't seem to like it when you try to update a View after its Activity has been sent to the background.

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

    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.

    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.

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

    Please take a look here: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety This is the correct solution and should always be used instead of all other workarounds.

    “You can ensure that the internal invocation list always has at least one member by initializing it with a do-nothing anonymous method. Because no external party can have a reference to the anonymous method, no external party can remove the method, so the delegate will never be null” — Programming .NET Components, 2nd Edition, by Juval Löwy

    public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  
    
    public static void OnPreInitializedEvent(EventArgs e)  
    {  
        // No check required - event will never be null because  
        // we have subscribed an empty anonymous delegate which  
        // can never be unsubscribed. (But causes some overhead.)  
        PreInitializedEvent(null, e);  
    }  
    
    0 讨论(0)
  • 2020-11-22 06:24

    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

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

    I truly enjoyed this read - not! Even though I need it to work with the C# feature called events!

    Why not fix this in the compiler? I know there are MS people who read these posts, so please don't flame this!

    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!

    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?

    Conclusion,

    Are not modern day languages supposed to solve problems like these for us?

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

    Thanks for a useful discussion. I was working on this problem recently and made the following class which is a bit slower, but allows to avoid callings to disposed objects.

    The main point here is that invocation list can be modified even event is raised.

    /// <summary>
    /// Thread safe event invoker
    /// </summary>
    public sealed class ThreadSafeEventInvoker
    {
        /// <summary>
        /// Dictionary of delegates
        /// </summary>
        readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();
    
        /// <summary>
        /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
        /// modification inside of it
        /// </summary>
        readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();
    
        /// <summary>
        /// locker for delegates list
        /// </summary>
        private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();
    
        /// <summary>
        /// Add delegate to list
        /// </summary>
        /// <param name="value"></param>
        public void Add(Delegate value)
        {
            var holder = new DelegateHolder(value);
            if (!delegates.TryAdd(value, holder)) return;
    
            listLocker.EnterWriteLock();
            delegatesList.AddLast(holder);
            listLocker.ExitWriteLock();
        }
    
        /// <summary>
        /// Remove delegate from list
        /// </summary>
        /// <param name="value"></param>
        public void Remove(Delegate value)
        {
            DelegateHolder holder;
            if (!delegates.TryRemove(value, out holder)) return;
    
            Monitor.Enter(holder);
            holder.IsDeleted = true;
            Monitor.Exit(holder);
        }
    
        /// <summary>
        /// Raise an event
        /// </summary>
        /// <param name="args"></param>
        public void Raise(params object[] args)
        {
            DelegateHolder holder = null;
    
            try
            {
                // get root element
                listLocker.EnterReadLock();
                var cursor = delegatesList.First;
                listLocker.ExitReadLock();
    
                while (cursor != null)
                {
                    // get its value and a next node
                    listLocker.EnterReadLock();
                    holder = cursor.Value;
                    var next = cursor.Next;
                    listLocker.ExitReadLock();
    
                    // lock holder and invoke if it is not removed
                    Monitor.Enter(holder);
                    if (!holder.IsDeleted)
                        holder.Action.DynamicInvoke(args);
                    else if (!holder.IsDeletedFromList)
                    {
                        listLocker.EnterWriteLock();
                        delegatesList.Remove(cursor);
                        holder.IsDeletedFromList = true;
                        listLocker.ExitWriteLock();
                    }
                    Monitor.Exit(holder);
    
                    cursor = next;
                }
            }
            catch
            {
                // clean up
                if (listLocker.IsReadLockHeld)
                    listLocker.ExitReadLock();
                if (listLocker.IsWriteLockHeld)
                    listLocker.ExitWriteLock();
                if (holder != null && Monitor.IsEntered(holder))
                    Monitor.Exit(holder);
    
                throw;
            }
        }
    
        /// <summary>
        /// helper class
        /// </summary>
        class DelegateHolder
        {
            /// <summary>
            /// delegate to call
            /// </summary>
            public Delegate Action { get; private set; }
    
            /// <summary>
            /// flag shows if this delegate removed from list of calls
            /// </summary>
            public bool IsDeleted { get; set; }
    
            /// <summary>
            /// flag shows if this instance was removed from all lists
            /// </summary>
            public bool IsDeletedFromList { get; set; }
    
            /// <summary>
            /// Constuctor
            /// </summary>
            /// <param name="d"></param>
            public DelegateHolder(Delegate d)
            {
                Action = d;
            }
        }
    }
    

    And the usage is:

        private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
        public event Action SomeEvent
        {
            add { someEventWrapper.Add(value); }
            remove { someEventWrapper.Remove(value); }
        }
    
        public void RaiseSomeEvent()
        {
            someEventWrapper.Raise();
        }
    

    Test

    I tested it in the following manner. I have a thread which creates and destroys objects like this:

    var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
    Thread.Sleep(10);
    objects.ForEach(x => x.Dispose());
    

    In a Bar (a listener object) constructor I subscribe to SomeEvent (which is implemented as shown above) and unsubscribe in Dispose:

        public Bar(Foo foo)
        {
            this.foo = foo;
            foo.SomeEvent += Handler;
        }
    
        public void Handler()
        {
            if (disposed)
                Console.WriteLine("Handler is called after object was disposed!");
        }
    
        public void Dispose()
        {
            foo.SomeEvent -= Handler;
            disposed = true;
        }
    

    Also I have couple of threads which raise event in a loop.

    All these actions are performed simultaneously: many listeners are created and destroyed and event is being fired at the same time.

    If there were a race conditions I should see a message in a console, but it is empty. But if I use clr events as usual I see it full of warning messages. So, I can conclude that it is possible to implement a thread safe events in c#.

    What do you think?

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