El SynchronizationContext actual no se puede utilizar como TaskScheduler

98

Estoy usando Tasks para ejecutar llamadas de servidor de larga duración en mi ViewModel y los resultados se calculan de nuevo al Dispatcherusar TaskScheduler.FromSyncronizationContext(). Por ejemplo:

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

Esto funciona bien cuando ejecuto la aplicación. Pero cuando ejecuto mis NUnitpruebas Resharper, aparece el mensaje de error en la llamada a FromCurrentSynchronizationContext:

El SynchronizationContext actual no se puede utilizar como TaskScheduler.

Supongo que esto se debe a que las pruebas se ejecutan en subprocesos de trabajo. ¿Cómo puedo asegurarme de que las pruebas se ejecuten en el hilo principal? Cualquier otra sugerencia es bienvenida.

anivas
fuente
en mi caso, estaba usando TaskScheduler.FromCurrentSynchronizationContext()dentro de una lambda y la ejecución se aplazó a otro hilo. obtener el contexto fuera de lambda solucionó el problema.
M.kazem Akhgary

Respuestas:

145

Debe proporcionar un SynchronizationContext. Así es como lo manejo:

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
fuente
6
Para MSTest: coloque el código anterior en el método marcado con ClassInitializeAttribute.
Daniel Bişar
6
@SACO: En realidad, tengo que ponerlo en el método con TestInitializeAttribute, de lo contrario, solo pasa la primera prueba.
Thorarin
2
Para las pruebas de xunit, lo puse en el tipo estático ctor, ya que solo necesita ser configurado una vez por dispositivo.
codekaizen
3
No entiendo en absoluto por qué se aceptó esta respuesta como la solución. NO FUNCIONA. Y la razón es simple: SynchronizationContext es una clase ficticia cuya función de envío / publicación es inútil. Esta clase debería ser abstracta en lugar de una clase concreta que posiblemente lleve a la gente a una falsa sensación de que "está funcionando". @tofutim Probablemente desee proporcionar su propia implementación derivada de SyncContext.
h9uest
1
Creo que lo descubrí. Mi TestInitialize es asincrónico. Cada vez que hay un "await" en TestInit, se pierde el SynchronizationContext actual. Esto se debe a que (como señaló @ h9uest), la implementación predeterminada de SynchronizationContext solo pone en cola las tareas en ThreadPool y en realidad no continúa en el mismo hilo.
Sapph
24

La solución de Ritch Melton no funcionó para mí. Esto se debe a que mi TestInitializefunción es asíncrona, al igual que mis pruebas, por lo que con cada awaituna SynchronizationContextse pierde la corriente . Esto se debe a que, como señala MSDN, la SynchronizationContextclase es "tonta" y solo pone en cola todo el trabajo en el grupo de subprocesos.

Lo que funcionó para mí es en realidad omitir la FromCurrentSynchronizationContextllamada cuando no hay un SynchronizationContext(es decir, si el contexto actual es nulo ). Si no hay un hilo de IU, no necesito sincronizarme con él en primer 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;
}

Encontré esta solución más sencilla que las alternativas, que donde:

  • Pase TaskSchedulera al ViewModel (a través de la inyección de dependencia)
  • Cree una prueba SynchronizationContexty un subproceso de interfaz de usuario "falso" para que se ejecuten las pruebas; muchos más problemas para mí de los que vale la pena

Pierdo algunos de los matices del subproceso, pero no estoy probando explícitamente que mis devoluciones de llamada OnPropertyChanged se activen en un subproceso específico, así que estoy de acuerdo con eso. De new SynchronizationContext()todos modos, las otras respuestas que usan realmente no funcionan mejor para ese objetivo.

Zafiro
fuente
Su elsecaso también fallará en una aplicación de servicio de Windows, lo que resultarásyncContextScheduler == null
FindOutIslamNow
Encontré el mismo problema, pero en cambio leí el código fuente de NUnit. AsyncToSyncAdapter solo anula su SynchronizationContext si se ejecuta en un subproceso de STA. Una solución alternativa es marcar su clase con un [RequiresThread]atributo.
Aron
1

He combinado varias soluciones para tener garantía de funcionamiento 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
fuente