【问题标题】:The current SynchronizationContext may not be used as a TaskScheduler当前的 SynchronizationContext 不能用作 TaskScheduler
【发布时间】:2012-01-04 23:10:30
【问题描述】:

我正在使用Tasks 在我的 ViewModel 中运行长时间运行的服务器调用,结果使用TaskScheduler.FromSyncronizationContext() 编组回Dispatcher。例如:

var context = TaskScheduler.FromCurrentSynchronizationContext();
this.Message = "Loading...";
Task task = Task.Factory.StartNew(() => { ... })
            .ContinueWith(x => this.Message = "Completed"
                          , context);

当我执行应用程序时,这工作正常。但是当我在Resharper 上运行我的NUnit 测试时,我在调用FromCurrentSynchronizationContext 时收到错误消息:

当前的 SynchronizationContext 不能用作 TaskScheduler。

我猜这是因为测试是在工作线程上运行的。如何确保测试在主线程上运行?欢迎任何其他建议。

【问题讨论】:

  • 在我的情况下,我在 lambda 中使用 TaskScheduler.FromCurrentSynchronizationContext() 并且执行被推迟到另一个线程。在 lambda 之外获取上下文解决了这个问题。

标签: c# multithreading nunit task-parallel-library resharper-6.0


【解决方案1】:

我已经结合了多种解决方案来保证 SynchronizationContext 工作:

using System;
using System.Threading;
using System.Threading.Tasks;

public class CustomSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback action, object state)
    {
        SendOrPostCallback actionWrap = (object state2) =>
        {
            SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext());
            action.Invoke(state2);
        };
        var callback = new WaitCallback(actionWrap.Invoke);
        ThreadPool.QueueUserWorkItem(callback, state);
    }
    public override SynchronizationContext CreateCopy()
    {
        return new CustomSynchronizationContext();
    }
    public override void Send(SendOrPostCallback d, object state)
    {
        base.Send(d, state);
    }
    public override void OperationStarted()
    {
        base.OperationStarted();
    }
    public override void OperationCompleted()
    {
        base.OperationCompleted();
    }

    public static TaskScheduler GetSynchronizationContext() {
      TaskScheduler taskScheduler = null;

      try
      {
        taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
      } catch {}

      if (taskScheduler == null) {
        try
        {
          taskScheduler = TaskScheduler.Current;
        } catch {}
      }

      if (taskScheduler == null) {
        try
        {
          var context = new CustomSynchronizationContext();
          SynchronizationContext.SetSynchronizationContext(context);
          taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        } catch {}
      }

      return taskScheduler;
    }
}

用法:

var context = CustomSynchronizationContext.GetSynchronizationContext();

if (context != null) 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... }, context);
}
else 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... });
}

【讨论】:

    【解决方案2】:

    Ritch Melton 的解决方案对我不起作用。这是因为我的TestInitialize 函数是异步的,我的测试也是如此,所以每个await 都会丢失当前的SynchronizationContext。这是因为正如 MSDN 指出的那样,SynchronizationContext 类是“哑”的,只是将所有工作排入线程池。

    对我有用的实际上只是在没有SynchronizationContext 时跳过FromCurrentSynchronizationContext 调用(即,如果当前上下文为null)。如果没有 UI 线程,我一开始就不需要同步。

    TaskScheduler syncContextScheduler;
    if (SynchronizationContext.Current != null)
    {
        syncContextScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    }
    else
    {
        // If there is no SyncContext for this thread (e.g. we are in a unit test
        // or console scenario instead of running in an app), then just use the
        // default scheduler because there is no UI thread to sync with.
        syncContextScheduler = TaskScheduler.Current;
    }
    

    我发现这个解决方案比其他解决方案更简单,其中:

    • TaskScheduler 传递给 ViewModel(通过依赖注入)
    • 创建一个测试 SynchronizationContext 和一个“假” UI 线程以运行测试 - 对我来说更麻烦的是值得

    我失去了一些线程细微差别,但我没有明确测试我的 OnPropertyChanged 回调是否在特定线程上触发,所以我可以接受。无论如何,使用new SynchronizationContext() 的其他答案并没有真正为该目标做得更好。

    【讨论】:

    • 您的 else 案例在 Windows 服务应用程序中也会失败,导致 syncContextScheduler == null
    • 遇到了同样的问题,但我阅读了 NUnit 源代码。 AsyncToSyncAdapter 只有在 STA 线程中运行时才会覆盖 SynchronizationContext。一种解决方法是使用 [RequiresThread] 属性标记您的类。
    【解决方案3】:

    您需要提供一个 SynchronizationContext。我是这样处理的:

    [SetUp]
    public void TestSetUp()
    {
      SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    }
    

    【讨论】:

    • 对于MSTest:将上面的代码放在标有ClassInitializeAttribute的Method中。
    • @SACO:其实我得放到TestInitializeAttribute的方法中,否则只有第一个测试通过。
    • 对于 xunit 测试,我把它放在静态类型 ctor 中,因为每个夹具只需要设置一次。
    • 我完全不明白为什么这个答案被接受为解决方案。这是行不通的。原因很简单:SynchronizationContext 是一个虚拟类,其发送/发布功能无用。这个类应该是抽象的,而不是一个可能导致人们误以为“它正在工作”的具体类。 @tofutim 您可能希望提供您自己的从 SyncContext 派生的实现。
    • 我想我明白了。我的 TestInitialize 是异步的。每次在 TestInit 中有一个“等待”,当前的 SynchronizationContext 就会丢失。这是因为(正如@h9uest 指出的那样), SynchronizationContext 的默认实现只是将任务排队到 ThreadPool 中,实际上并没有在同一个线程上继续。
    猜你喜欢
    • 1970-01-01
    • 2014-03-24
    • 1970-01-01
    • 1970-01-01
    • 2012-03-23
    • 2016-03-03
    • 1970-01-01
    • 1970-01-01
    • 2013-04-24
    相关资源
    最近更新 更多