O SynchronizationContext atual não pode ser usado como um TaskScheduler

98

Estou usando o Tasks para executar chamadas de servidor de longa duração em meu ViewModel e os resultados são encaminhados de volta ao Dispatcheruso TaskScheduler.FromSyncronizationContext(). Por exemplo:

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

Isso funciona bem quando executo o aplicativo. Mas quando eu executo meus NUnittestes em Resharper, recebo a mensagem de erro na chamada para FromCurrentSynchronizationContextas:

O SynchronizationContext atual não pode ser usado como TaskScheduler.

Acho que isso ocorre porque os testes são executados em threads de trabalho. Como posso garantir que os testes sejam executados no thread principal? Quaisquer outras sugestões são bem-vindas.

anivas
fonte
no meu caso, eu estava usando TaskScheduler.FromCurrentSynchronizationContext()dentro de um lambda e a execução foi adiada para outro thread. obter o contexto fora de lambda corrigiu o problema.
M.kazem Akhgary

Respostas:

145

Você precisa fornecer um SynchronizationContext. É assim que eu lido com isso:

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
fonte
6
Para MSTest: coloque o código acima no Método marcado com ClassInitializeAttribute.
Daniel Bişar
6
@SACO: Na verdade, tenho que colocá-lo em um método com TestInitializeAttribute, caso contrário, só passa o primeiro teste.
Thorarin
2
Para testes xunit, eu coloquei no ctor do tipo estático, uma vez que ele só precisa ser configurado uma vez por dispositivo.
codekaizen
3
Não compreendo de todo porque esta resposta foi aceite como solução. NÃO FUNCIONA. E a razão é simples: SynchronizationContext é uma classe fictícia cujas funções enviar / postar são inúteis. Esta classe deve ser abstrata ao invés de uma classe concreta que possivelmente leva as pessoas a uma falsa sensação de "está funcionando". @tofutim Você provavelmente deseja fornecer sua própria implementação derivada de SyncContext.
h9uest
1
Eu acho que descobri. Meu TestInitialize é assíncrono. Cada vez que há uma "espera" no TestInit, o SynchronizationContext atual é perdido. Isso ocorre porque (como @ h9uest apontou), a implementação padrão do SynchronizationContext apenas enfileira tarefas no ThreadPool e não continua na mesma thread.
Sapph de
24

A solução de Ritch Melton não funcionou para mim. Isso ocorre porque minha TestInitializefunção é assíncrona, assim como meus testes, portanto, a cada momento awaita corrente SynchronizationContexté perdida. Isso ocorre porque, como aponta o MSDN, a SynchronizationContextclasse é "burra" e apenas enfileira todo o trabalho para o pool de threads.

O que funcionou para mim foi simplesmente ignorar a FromCurrentSynchronizationContextchamada quando não havia um SynchronizationContext(isto é, se o contexto atual for nulo ). Se não houver UI thread, não preciso sincronizar com ele em primeiro lugar.

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

Achei essa solução mais direta do que as alternativas, que eram:

  • Passe um TaskSchedulerpara o ViewModel (via injeção de dependência)
  • Crie um teste SynchronizationContexte um thread de IU "falso" para os testes serem executados - muito mais problemas para mim que vale a pena

Perco algumas nuances de threading, mas não estou testando explicitamente se meus retornos de chamada OnPropertyChanged são acionados em um thread específico, portanto, estou bem com isso. As outras respostas usando new SynchronizationContext()realmente não fazem nada melhor para esse objetivo de qualquer maneira.

Sapph
fonte
Seu elsecaso irá falhar também em um aplicativo de serviço do Windows, resultandosyncContextScheduler == null
FindOutIslamNow
Encontrei o mesmo problema, mas em vez disso li o código-fonte do NUnit. AsyncToSyncAdapter apenas substitui seu SynchronizationContext se ele estiver sendo executado em um thread STA. Uma solução alternativa é marcar sua classe com um [RequiresThread]atributo.
Aron
1

Combinei várias soluções para ter garantia de funcionamento do 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;
    }
}

Uso:

var context = CustomSynchronizationContext.GetSynchronizationContext();

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