开发者

Looking for an example of a custom SynchronizationContext (Required for unit testing)

开发者 https://www.devze.com 2022-12-13 21:42 出处:网络
I need a custom SynchronizationContext that:开发者_运维问答 Owns a single thread that runs \"Posts\" and \"Sends\" delegates

I need a custom SynchronizationContext that:

开发者_运维问答
  • Owns a single thread that runs "Posts" and "Sends" delegates
  • Does the send in the order they are send in
  • No other methods are needed

I need this so I can unit test some threading code that will talk to WinForm in the real application.

Before I write my own, I was hoping that someone could point me to a simple (and small) implementations.


This one was written by me some time ago, no issues with copyright, no guarantees either(the system didn't go into production):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows.Threading;

namespace ManagedHelpers.Threads
{
    public class STASynchronizationContext : SynchronizationContext, IDisposable
    {
        private readonly Dispatcher dispatcher;
        private object dispObj;
        private readonly Thread mainThread;

        public STASynchronizationContext()
        {
            mainThread = new Thread(MainThread) { Name = "STASynchronizationContextMainThread", IsBackground = false };
            mainThread.SetApartmentState(ApartmentState.STA);
            mainThread.Start();

            //wait to get the main thread's dispatcher
            while (Thread.VolatileRead(ref dispObj) == null)
                Thread.Yield();

            dispatcher = dispObj as Dispatcher;
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            dispatcher.BeginInvoke(d, new object[] { state });
        }

        public override void Send(SendOrPostCallback d, object state)
        {
            dispatcher.Invoke(d, new object[] { state });
        }

        private void MainThread(object param)
        {
            Thread.VolatileWrite(ref dispObj, Dispatcher.CurrentDispatcher);
            Console.WriteLine("Main Thread is setup ! Id = {0}", Thread.CurrentThread.ManagedThreadId);
            Dispatcher.Run();
        }

        public void Dispose()
        {
            if (!dispatcher.HasShutdownStarted && !dispatcher.HasShutdownFinished)
                dispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);

            GC.SuppressFinalize(this);
        }

        ~STASynchronizationContext()
        {
            Dispose();
        }
    }
}


idesign.net (search for Custom Synchronization Context on the page) has a SynchronizationContext that will do the job, however it is more complex them I need.


Had a similar requirement - unit testing a server component to confirm that it's callback delegate invocations were marshalled onto an appropriate SynchronizationContext and came up with the following code (based on Stephen Toub's blog post http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx) which I recon is simpler and more general as it uses it's own internal thread to service the Post()/Send() requests, rather than relying on WPF/Winforms/.. to perform dispatching.

    // A simple SynchronizationContext that encapsulates it's own dedicated task queue and processing
    // thread for servicing Send() & Post() calls.  
    // Based upon http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx but uses it's own thread
    // rather than running on the thread that it's instanciated on
    public sealed class DedicatedThreadSynchronisationContext : SynchronizationContext, IDisposable
    {
        public DedicatedThreadSynchronisationContext()
        {
            m_thread = new Thread(ThreadWorkerDelegate);
            m_thread.Start(this);
        }

        public void Dispose()
        {
            m_queue.CompleteAdding();
        }

        /// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
        /// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
        /// <param name="state">The object passed to the delegate.</param>
        public override void Post(SendOrPostCallback d, object state)
        {
            if (d == null) throw new ArgumentNullException("d");
            m_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
        }

        /// <summary> As 
        public override void Send(SendOrPostCallback d, object state)
        {
            using (var handledEvent = new ManualResetEvent(false))
            {
                Post(SendOrPostCallback_BlockingWrapper, Tuple.Create(d, state, handledEvent));
                handledEvent.WaitOne();
            }
        }

        public int WorkerThreadId { get { return m_thread.ManagedThreadId; } }
        //=========================================================================================

        private static void SendOrPostCallback_BlockingWrapper(object state)
        {
            var innerCallback = (state as Tuple<SendOrPostCallback, object, ManualResetEvent>);
            try
            {
                innerCallback.Item1(innerCallback.Item2);
            }
            finally
            {
                innerCallback.Item3.Set();
            }
        }

        /// <summary>The queue of work items.</summary>
        private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
            new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

        private readonly Thread m_thread = null;

        /// <summary>Runs an loop to process all queued work items.</summary>
        private void ThreadWorkerDelegate(object obj)
        {
            SynchronizationContext.SetSynchronizationContext(obj as SynchronizationContext);

            try
            {
                foreach (var workItem in m_queue.GetConsumingEnumerable())
                    workItem.Key(workItem.Value);
            }
            catch (ObjectDisposedException) { }
        }
    }   


Some modern unit testing libraries (e.g., xUnit) already include single-threaded SynchronizationContext instances by default. Others do not (e.g., MSTest).

My AsyncEx library has an AsyncContext that installs a single-threaded SynchronizationContext and just processes the queue of messages on that single thread. Usage is quite simple:

public Task MyTestMethod() => AsyncContext.Run(async () =>
{
  // asynchronous code here.
});

AsyncContext was designed for unit tests and Console applications, and has proper handling of some more exotic scenarios, e.g.:

  • async void methods are detected and the Run method will not return until they are complete.
  • AsyncContext.Run allows returning a result value, useful if you want to do your assertions outside of the asynchronous code.
  • If the delegate passed to Run propagates an exception or if any exception is propagated out of an async void method, then that exception propagates out of AsyncContext.Run (without exception wrappers and preserving the exception call stack).

However, AsyncContext is just a SynchronizationContext (with a "runner"), and has no notion of UI-specific thread synchronization mechanisms (e.g., Dispatcher, Control.Invoke). If you need to test code that uses a dispatcher or control, then you'll need to use WpfContext or WindowsFormsContext, which were helper types from the original Async CTP.


I have adapted the answer by Bond to remove the dependency on WPF (Dispatcher), and depend on WinForms instead:

namespace ManagedHelpers.Threads
   {
   using System;
   using System.Collections.Generic;
   using System.Diagnostics;
   using System.Linq;
   using System.Text;
   using System.Threading;
   using System.Threading.Tasks;
   using System.Windows.Forms;
   using NUnit.Framework;

   public class STASynchronizationContext : SynchronizationContext, IDisposable
      {
      private readonly Control control;
      private readonly int mainThreadId;

      public STASynchronizationContext()
         {
         this.control = new Control();

         this.control.CreateControl();

         this.mainThreadId = Thread.CurrentThread.ManagedThreadId;

         if (Thread.CurrentThread.Name == null)
            {
            Thread.CurrentThread.Name = "AsynchronousTestRunner Main Thread";
            }
         }

      public override void Post(SendOrPostCallback d, object state)
         {
         control.BeginInvoke(d, new object[] { state });
         }

      public override void Send(SendOrPostCallback d, object state)
         {
         control.Invoke(d, new object[] { state });
         }

      public void Dispose()
         {
         Assert.AreEqual(this.mainThreadId, Thread.CurrentThread.ManagedThreadId);

         this.Dispose(true);
         GC.SuppressFinalize(this);
         }

      protected virtual void Dispose(bool disposing)
         {
         Assert.AreEqual(this.mainThreadId, Thread.CurrentThread.ManagedThreadId);

         if (disposing)
            {
            if (control != null)
               {
               control.Dispose();
               }
            }
         }

      ~STASynchronizationContext()
         {
         this.Dispose(false);
         }
      }
   }
0

精彩评论

暂无评论...
验证码 换一张
取 消