当前的 SynchronizationContext 不能用作任务计划程序

我使用 任务在我的 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 不能用作任务计划程序。

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

42607 次浏览

You need to provide a SynchronizationContext. This is how I handle it:

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

Ritch Melton's solution did not work for me. This is because my TestInitialize function is async, as are my tests, so with every await the current SynchronizationContext is lost. This is because as MSDN points out, the SynchronizationContext class is "dumb" and just queues all work to the thread pool.

What worked for me is actually just skipping over the FromCurrentSynchronizationContext call when there isn't a SynchronizationContext (that is, if the current context is null). If there's no UI thread, I don't need to synchronize with it in the first place.

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

I found this solution more straightforward than the alternatives, which where:

  • Pass a TaskScheduler to the ViewModel (via dependency injection)
  • Create a test SynchronizationContext and a "fake" UI thread for the tests to run on - way more trouble for me that it's worth

I lose some of the threading nuance, but I am not explicitly testing that my OnPropertyChanged callbacks trigger on a specific thread so I am okay with that. The other answers using new SynchronizationContext() don't really do any better for that goal anyway.

I have combined multiple solution to have guarantee for working 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;
}
}

Usage:

var context = CustomSynchronizationContext.GetSynchronizationContext();


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