Текущий контекст синхронизации нельзя использовать в качестве планировщика задач.

98

Я использую задачи для выполнения длительных вызовов сервера в моей модели просмотра, и результаты снова упорядочиваются при 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.

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

анивас
источник
в моем случае я использовал TaskScheduler.FromCurrentSynchronizationContext()лямбда-выражение, и выполнение было отложено до другого потока. получение контекста за пределами лямбда решило проблему.
М.казем Ахгары

Ответы:

145

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

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ритч Мелтон
источник
6
Для MSTest: поместите приведенный выше код в метод, отмеченный атрибутом ClassInitializeAttribute.
Daniel Bişar 09
6
@SACO: На самом деле я должен поместить его в метод with TestInitializeAttribute, иначе пройдет только первый тест.
Thorarin
2
Для тестов xunit я помещаю его в ctor статического типа, поскольку его нужно настроить только один раз для каждого прибора.
codekaizen
3
Совершенно не понимаю, почему этот ответ был принят как решение. ЭТО НЕ РАБОТАЕТ. Причина проста: SynchronizationContext - это фиктивный класс, функции отправки и отправки которого бесполезны. Этот класс должен быть абстрактным, а не конкретным классом, который, возможно, вводит людей в ложное ощущение того, что «он работает». @tofutim Вы, вероятно, захотите предоставить свою собственную реализацию, производную от SyncContext.
h9uest 02
1
Я думаю, что разобрался. Мой TestInitialize асинхронный. Каждый раз, когда в TestInit присутствует «ожидание», текущий контекст SynchronizationContext теряется. Это связано с тем, что (как указал @ h9uest), реализация SynchronizationContext по умолчанию просто ставит задачи в очередь ThreadPool и фактически не продолжает работу в том же потоке.
Sapph
24

Решение Ритча Мелтона не сработало для меня. Это потому, что моя TestInitializeфункция асинхронная, как и мои тесты, поэтому с каждым awaitтоком SynchronizationContextтеряется. Это связано с тем, что, как указывает MSDN, SynchronizationContextкласс «тупой» и просто ставит в очередь всю работу в пуле потоков.

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

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

Я нашел это решение более простым, чем альтернативы, в которых:

  • Передайте TaskSchedulerViewModel (через внедрение зависимостей)
  • Создать тест SynchronizationContextи «поддельный» поток пользовательского интерфейса для запуска тестов - для меня больше проблем, чем оно того стоит

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

Сапф
источник
Ваш elseслучай также потерпит неудачу в приложении службы Windows, в результатеsyncContextScheduler == null
FindOutIslamNow
Столкнулся с той же проблемой, но вместо этого я прочитал исходный код NUnit. AsyncToSyncAdapter переопределяет ваш SynchronizationContext, только если он выполняется в потоке STA. Обходной путь - пометить свой класс [RequiresThread]атрибутом.
Aron
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 => { ... });
}
ujeenator
источник