当前的同步上下文不能用作任务调度器。

107

我正在使用 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时会出现以下错误消息:

当前同步上下文不能用作任务计划程序。

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


在我的情况下,我在 lambda 中使用了 TaskScheduler.FromCurrentSynchronizationContext(),导致执行被延迟到另一个线程。在 lambda 外获取上下文可以解决这个问题。 - M.kazem Akhgary
3个回答

156

您需要提供一个SynchronizationContext。以下是我处理它的方式:

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

7
对于 MSTest:将上述代码放在标有 ClassInitializeAttribute 的方法中。 - Daniel Bişar
7
@SACO:实际上,我必须将它放入一个带有TestInitializeAttribute方法中,否则只有第一个测试通过。 - Thorarin
2
对于xunit测试,我将其放在静态类型构造函数中,因为它只需要针对每个夹具设置一次。 - codekaizen
3
我完全不理解为什么这个答案被接受为解决方案。它不起作用。原因很简单:SynchronizationContext是一个虚拟类,其发送/发布函数是无用的。这个类应该是抽象的,而不是一个具体的类,可能会让人们产生错误的“它正在工作”的感觉。@tofutim,您可能想提供自己从SyncContext派生的实现。 - h9uest
1
我想我找到了答案。我的TestInitialize是异步的。每次在TestInit中有一个"await"时,当前的同步上下文都会丢失。这是因为(正如@h9uest所指出的那样),SynchronizationContext的默认实现只是将任务排队到线程池中,并没有在相同的线程上继续执行。 - Sapph
显示剩余5条评论

32

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

我认为这个解决方案比其他替代方案更加直观,那些替代方案包括:

  • 通过依赖注入向ViewModel传递TaskScheduler
  • 创建一个测试SynchronizationContext和一个“虚假”的UI线程,以供测试运行 - 对我来说带来了更多麻烦,而这并不值得

我失去了一些线程细微差别,但我没有明确测试我的OnPropertyChanged回调是否在特定线程上触发,因此我可以接受这一点。使用new SynchronizationContext()的其他答案也无法更好地实现该目标。


你的 else 情况在 Windows 服务应用程序中也会失败,导致 syncContextScheduler == null - FindOutIslamNow
遇到了相同的问题,但是我阅读了 NUnit 的源代码。AsyncToSyncAdapter 只会在 STA 线程中运行时覆盖您的 SynchronizationContext。解决方法是在您的类上标记 [RequiresThread] 属性。 - Aron

1

我已经结合了多个解决方案,以确保工作的同步上下文:

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 => { ... });
}

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