web-dev-qa-db-fra.com

Le SynchronizationContext actuel ne peut pas être utilisé en tant que TaskScheduler

J'utilise Tasks pour exécuter des appels de serveur longs dans mon ViewModel et les résultats sont rassemblés dans Dispatcher en utilisant TaskScheduler.FromSyncronizationContext(). Par exemple:

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

Cela fonctionne bien lorsque j'exécute l'application. Mais lorsque j'exécute mes tests NUnit sur Resharper, le message d'erreur relatif à l'appel à FromCurrentSynchronizationContext s'affiche de la manière suivante: 

Le SynchronizationContext actuel ne peut pas être utilisé en tant que TaskScheduler.

J'imagine que c'est parce que les tests sont exécutés sur des threads de travail. Comment puis-je m'assurer que les tests sont exécutés sur le thread principal? N'importe quelles autres suggestions sont les bienvenues.

94
anivas

Vous devez fournir un SynchronizationContext. Voici comment je le gère:

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

La solution de Ritch Melton n'a pas fonctionné pour moi. En effet, ma fonction TestInitialize est asynchrone, de même que mes tests. Ainsi, avec chaque await, la SynchronizationContext actuelle est perdue. En effet, comme le souligne MSDN, la classe SynchronizationContext est "idiote" et met en file d'attente tout le travail dans le pool de threads.

Ce qui a fonctionné pour moi est en fait de simplement ignorer l'appel FromCurrentSynchronizationContext lorsqu'il n'y a pas de SynchronizationContext (c'est-à-dire si le contexte actuel est null ). S'il n'y a pas de thread d'interface utilisateur, je n'ai pas besoin de synchroniser avec elle en premier lieu.

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

J'ai trouvé cette solution plus simple que les alternatives, où:

  • Transmettez une TaskScheduler au ViewModel (via une injection de dépendance)
  • Créez un test SynchronizationContext et un "faux" thread d'interface utilisateur pour que les tests s'exécutent de manière bien plus pénible qu'il en vaut la peine

Je perds une partie de la nuance des threads, mais je ne teste pas explicitement que mes rappels OnPropertyChanged se déclenchent sur un thread spécifique, ce qui me convient. Les autres réponses utilisant new SynchronizationContext() ne font pas vraiment mieux pour cet objectif de toute façon.

18
Sapph

J'ai combiné plusieurs solutions pour garantir le bon fonctionnement de 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 => { ... });
}
0
Ujeenator