寻找一个自定义SynchronizationContext的示例(用于单元测试)

18
我需要一个定制的同步上下文,它具有以下特点:
  • 拥有一个单独的线程来运行“发布”和“发送”委托
  • 按照发送的顺序进行发送
  • 不需要其他方法
我需要这个功能来对真实应用程序中与WinForm通信的线程代码进行单元测试。
在编写自己的代码之前,我希望有人能指导我使用简单(且小型)的实现。
5个回答

10

这是我一段时间以前写的,没有版权问题,也没有保证(系统没有进入生产):

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();
        }
    }
}

1
这看起来相当不错,而且很简单。它确实是在提问近3年后回答并被采纳的吗?!我将尝试一下,但“它可行吗?” - Jonathon Reinhart
@JonathonReinhart - 是的,在我的开发测试期间它确实有效,如果您发现任何问题,请告诉我,以便我们改进答案。 - Ventsyslav Raikov

8

idesign.net(在页面上搜索“自定义同步上下文”)有一个SynchronizationContext可以完成此任务,但它比我需要的更复杂。


1
链接已经彻底失效了。你能否包含一些创建自定义同步上下文的相关代码? - IAbstract
请访问http://idesign.net/Downloads获取代码,抱歉我不知道版权是否允许我将其放在答案中。 - Ian Ringrose

5

我有一个类似的需求——对服务器组件进行单元测试,以确认其回调委托调用是否被编排到适当的同步上下文,并想出了以下代码(基于Stephen Toub的博客文章http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx),我认为它更简单和通用,因为它使用自己内部的线程来处理Post()/Send()请求,而不是依赖于WPF/WinForms/..来执行调度。

    // 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) { }
        }
    }   

1
提到 Stephen Toub 的博客文章的当前地址为 https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/。 - ckuri

2
一些现代单元测试库(例如xUnit)已经默认包含单线程的SynchronizationContext实例,而其他一些则没有(例如MSTest)。
我的AsyncEx库有一个AsyncContext,它安装了一个单线程的SynchronizationContext并在该单个线程上处理消息队列。使用非常简单:
public Task MyTestMethod() => AsyncContext.Run(async () =>
{
  // asynchronous code here.
});

AsyncContext旨在用于单元测试和控制台应用程序,并且对一些更加特殊的情况有适当的处理,例如:

  • 检测到async void方法,并且Run方法将不会返回,直到它们完成。
  • AsyncContext.Run允许返回结果值,如果您想在异步代码之外进行断言,则非常有用。
  • 如果传递给Run的委托传播异常或者async void方法中传播任何异常,则该异常将从AsyncContext.Run传播出去(不带异常包装并保留异常调用堆栈)。

然而,AsyncContext仅是一个SynchronizationContext(带有"runner"),并且没有UI特定的线程同步机制(例如,DispatcherControl.Invoke)。如果您需要测试使用调度程序或控件的代码,则需要使用WpfContextWindowsFormsContext,这是最初的Async CTP中的辅助类型。


1

我已经修改了Bond的答案,以便不再依赖WPF(Dispatcher),而是改为依赖WinForms:

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);
         }
      }
   }

这基本上是 WinFormsSynchronizationContext 的重新实现,它做的大部分事情都是一样的。 - ckuri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接