C# Events and Thread Safety

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

    /// 
    /// Thread safe event invoker
    /// 
    public sealed class ThreadSafeEventInvoker
    {
        /// 
        /// Dictionary of delegates
        /// 
        readonly ConcurrentDictionary delegates = new ConcurrentDictionary();
    
        /// 
        /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
        /// modification inside of it
        /// 
        readonly LinkedList delegatesList = new LinkedList();
    
        /// 
        /// locker for delegates list
        /// 
        private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();
    
        /// 
        /// Add delegate to list
        /// 
        /// 
        public void Add(Delegate value)
        {
            var holder = new DelegateHolder(value);
            if (!delegates.TryAdd(value, holder)) return;
    
            listLocker.EnterWriteLock();
            delegatesList.AddLast(holder);
            listLocker.ExitWriteLock();
        }
    
        /// 
        /// Remove delegate from list
        /// 
        /// 
        public void Remove(Delegate value)
        {
            DelegateHolder holder;
            if (!delegates.TryRemove(value, out holder)) return;
    
            Monitor.Enter(holder);
            holder.IsDeleted = true;
            Monitor.Exit(holder);
        }
    
        /// 
        /// Raise an event
        /// 
        /// 
        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;
            }
        }
    
        /// 
        /// helper class
        /// 
        class DelegateHolder
        {
            /// 
            /// delegate to call
            /// 
            public Delegate Action { get; private set; }
    
            /// 
            /// flag shows if this delegate removed from list of calls
            /// 
            public bool IsDeleted { get; set; }
    
            /// 
            /// flag shows if this instance was removed from all lists
            /// 
            public bool IsDeletedFromList { get; set; }
    
            /// 
            /// Constuctor
            /// 
            /// 
            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?

提交回复
热议问题