跟踪来自多个线程的对象的WeakReference

时间:2012-01-11 20:17:01

标签: multithreading c#-4.0 delegates garbage-collection weak-references

我正在设计一个静态消息总线,它允许订阅和发布任意类型的消息。为了避免要求观察者明确取消订阅,我想跟踪指向代理的WeakReference对象,而不是跟踪代理本身。我最终编写了类似于Paul Stovell在他的博客http://www.paulstovell.com/weakevents中描述的内容。

我的问题是:与Paul的代码相反,我的观察者在一个线程上订阅消息,但消息可能在另一个线程上发布。在这种情况下,我观​​察到当我需要通知观察者时,我的WeakReference.Target值为null表示已经收集了目标,即使我确定它们不是。短期和长期弱引用都存在问题。

相反,当订阅和发布是从同一个线程完成时,代码工作正常。后者是真的,即使我实际上最终枚举来自ThreadPool的新线程上的目标,只要请求最初来自我订阅消息的同一线程。

我知道这是一个非常具体的案例,所以非常感谢任何帮助。

我的问题是:如果提供了正确的线程同步,我是否应该无法从多个线程中可靠地访问WeakReference对象?看来我不能,这对我来说没有多大意义。那么,我做得不对?

2 个答案:

答案 0 :(得分:0)

看起来在将代码缩减为更简单的形式(见下文)之后,它现在可以正常工作了。这意味着,导致过早收集弱参考目标的问题必须存在于我的代码的其他地方。因此,为了回答我自己的问题,似乎可以从多个线程安全地访问弱引用。

这是我的测试代码:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;


namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting the app");
            Test test = new Test();
            // uncomment these lines to cause automatic unsubscription from Message1
//          test = null;
//          GC.Collect();
//          GC.WaitForPendingFinalizers();

            // publish Message1 on this thread
            // MessageBus.Publish<Message1>(new Message1());

            // publish Message1 on another thread
            ThreadPool.QueueUserWorkItem(delegate
            {
                MessageBus.Publish<Message1>(new Message1());
            });


            while (!MessageBus.IamDone)
            {
                Thread.Sleep(100);
            }
            Console.WriteLine("Exiting the app");
            Console.WriteLine("Press <ENTER> to terminate program.");
            Console.WriteLine();
            Console.ReadLine();
        }
    }

    public class Test
    {
        public Test()
        {
            Console.WriteLine("Subscribing to message 1.");
            MessageBus.Subscribe<Message1>(OnMessage1);
            Console.WriteLine("Subscribing to message 2.");
            MessageBus.Subscribe<Message2>(OnMessage2);
        }

        public void OnMessage1(Message1 message)
        {
            Console.WriteLine("Got message 1. Publishing message 2");
            MessageBus.Publish<Message2>(new Message2());
        }

        public void OnMessage2(Message2 message)
        {
            Console.WriteLine("Got message 2. Closing the app");
            MessageBus.IamDone = true;
        }
    }

    public abstract class MessageBase
    {
        public string Message;
    }

    public class Message1 : MessageBase
    {
    }

    public class Message2 : MessageBase
    {
    }

    public static class MessageBus
    {
        // This is here purely for this test
        public static bool IamDone = false;
        /////////////////////////////////////

        /// <summary>
        /// A dictionary of lists of handlers of messages by message type
        /// </summary>
        private static ConcurrentDictionary<string, List<WeakReference>> handlersDict = new ConcurrentDictionary<string, List<WeakReference>>();

        /// <summary>
        /// Thread synchronization object to use with Publish calls
        /// </summary>
        private static object _lockPublishing = new object();

        /// <summary>
        /// Thread synchronization object to use with Subscribe calls
        /// </summary>
        private static object _lockSubscribing = new object();

        /// <summary>
        /// Creates a work queue item that encapsulates the provided parameterized message
        /// and dispatches it.
        /// </summary>
        /// <typeparam name="TMessage">Message argument type</typeparam>
        /// <param name="message">Message argument</param>
        public static void Publish<TMessage>(TMessage message)
            where TMessage : MessageBase
        {
            // create the dictionary key
            string key = String.Empty;
            key = typeof(TMessage).ToString();
            // initialize a queue work item argument as a tuple of the dictionary type key and the message argument
            Tuple<string, TMessage, Exception> argument = new Tuple<string, TMessage, Exception>(key, message, null);
            // push the message on the worker queue
            ThreadPool.QueueUserWorkItem(new WaitCallback(_PublishMessage<TMessage>), argument);
        }

        /// <summary>
        /// Publishes a message to the bus, causing observers to be invoked if appropriate.
        /// </summary>
        /// <typeparam name="TArg">Message argument type</typeparam>
        /// <param name="stateInfo">Queue work item argument</param>
        private static void _PublishMessage<TArg>(Object stateInfo)
            where TArg : class
        {
            try
            {
                // translate the queue work item argument to extract the message type info and
                // any arguments
                Tuple<string, TArg, Exception> arg = (Tuple<string, TArg, Exception>)stateInfo;
                // call all observers that have registered to receive this message type in parallel
                Parallel.ForEach(handlersDict.Keys
                    // find the right dictionary list entry by message type identifier
                    .Where(handlerKey => handlerKey == arg.Item1)
                    // dereference the list entry by message type identifier to get a reference to the observer
                    .Select(handlerKey => handlersDict[handlerKey]), (handlerList, state) =>
                    {
                        lock (_lockPublishing)
                        {
                            List<int> descopedRefIndexes = new List<int>(handlerList.Count);
                            // search the list of references and invoke registered observers
                            foreach (WeakReference weakRef in handlerList)
                            {
                                // try to obtain a strong reference to the target
                                Delegate dlgRef = (weakRef.Target as Delegate);
                                // check if the underlying delegate reference is still valid
                                if (dlgRef != null)
                                {
                                    // yes it is, get the delegate reference via Target property, convert it to Action and invoke the observer
                                    try
                                    {
                                        (dlgRef as Action<TArg>).Invoke(arg.Item2);
                                    }
                                    catch (Exception e)
                                    {
                                        // trouble invoking the target observer's reference, mark it for deletion
                                        descopedRefIndexes.Add(handlerList.IndexOf(weakRef));
                                        Console.WriteLine(String.Format("Error looking up target reference: {0}", e.Message));
                                    }
                                }
                                else
                                {
                                    // the target observer's reference has been descoped, mark it for deletion
                                    descopedRefIndexes.Add(handlerList.IndexOf(weakRef));
                                    Console.WriteLine(String.Format("Message type \"{0}\" has been unsubscribed from.", arg.Item1));
                                    MessageBus.IamDone = true;
                                }
                            }
                            // remove any descoped references
                            descopedRefIndexes.ForEach(index => handlerList.RemoveAt(index));
                        }
                    });
            }
            // catch all Exceptions
            catch (AggregateException e)
            {
                Console.WriteLine(String.Format("Error dispatching messages: {0}", e.Message));
            }
        }

        /// <summary>
        /// Subscribes the specified delegate to handle messages of type TMessage
        /// </summary>
        /// <typeparam name="TArg">Message argument type</typeparam>
        /// <param name="action">WeakReference that represents the handler for this message type to be registered with the bus</param>
        public static void Subscribe<TArg>(Action<TArg> action)
            where TArg : class
        {
            // validate input
            if (action == null)
                throw new ArgumentNullException(String.Format("Error subscribing to message type \"{0}\": Specified action reference is null.", typeof(TArg)));
            // build the queue work item key identifier
            string key = typeof(TArg).ToString();
            // check if a message of this type was already added to the bus
            if (!handlersDict.ContainsKey(key))
            {
                // no, it was not, create a new dictionary entry and add the new observer's reference to it
                List<WeakReference> newHandlerList = new List<WeakReference>();
                handlersDict.TryAdd(key, newHandlerList);
            }
            lock (_lockSubscribing)
            {
                // append this new observer's reference to the list, if it does not exist already
                if (!handlersDict[key].Any(existing => (existing.Target as Delegate) != null && (existing.Target as Delegate).Equals(action)))
                {
                    // append the new reference
                    handlersDict[key].Add(new WeakReference(action, true));
                }
            }
        }
    }
}

答案 1 :(得分:0)

这是我之前回答的修正案。我发现为什么我的原始代码不起作用,这些信息可能对其他人有用。在我的原始代码中,MessageBus被实例化为Singleton:

public class MessageBus : Singleton<MessageBus> // Singleton<> is my library class

在上面的示例中,它被声明为static:

public static class MessageBus

一旦我将代码转换为使用静态,事情就开始起作用了。话虽如此,我还不知道为什么单身人士不起作用。