Текущий SynchronizationContext не может использоваться как TaskScheduler

Я использую Tasks для запуска длинных вызовов сервера в моей модели ViewModel, и результаты снова сортируются по Dispatcher с помощью TaskScheduler.FromSyncronizationContext(). Например:

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

Это отлично работает при выполнении приложения. Но когда я запускаю тесты NUnit на Resharper, я получаю сообщение об ошибке при вызове FromCurrentSynchronizationContext как:

Текущий SynchronizationContext не может использоваться как TaskScheduler.

Я предполагаю, что это происходит потому, что тесты выполняются в рабочих потоках. Как я могу обеспечить, чтобы тесты выполнялись в основном потоке? Любые другие предложения приветствуются.

Ответ 1

Вам необходимо предоставить SynchronizationContext. Вот как я справляюсь с этим:

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

Ответ 2

Решение Ritch Melton для меня не сработало. Это связано с тем, что моя функция TestInitialize является асинхронной, как и мои тесты, поэтому с каждым await текущий SynchronizationContext теряется. Это связано с тем, что, как указывает MSDN, класс SynchronizationContext является "тупым" и просто выполняет все очереди в пуле потоков.

То, что сработало для меня, на самом деле просто пропускает вызов FromCurrentSynchronizationContext, когда нет SynchronizationContext (то есть, если текущий контекст null). Если нет потока пользовательского интерфейса, мне не нужно сначала синхронизировать его.

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 и "поддельный" пользовательский интерфейс для тестов, которые будут выполняться в пути больше, для меня, что стоит

Я теряю часть нюансов потоков, но я не проверяю, что мои вызовы OnPropertyChanged запускаются по определенному потоку, поэтому я в порядке с этим. Другие ответы, использующие new SynchronizationContext(), в любом случае не лучше для этой цели.

Ответ 3

Я объединил несколько решений, чтобы иметь гарантию на работу 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 => { ... });
}