Le SynchronizationContext actuel ne peut pas être utilisé comme un TaskScheduler

98

J'utilise des tâches pour exécuter des appels de serveur de longue durée dans mon ViewModel et les résultats sont rassemblés lors de l' Dispatcherutilisation 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 quand j'exécute mes NUnittests, Resharperj'obtiens le message d'erreur sur l'appel à FromCurrentSynchronizationContextcomme:

Le SynchronizationContext actuel ne peut pas être utilisé comme un TaskScheduler.

Je suppose 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.

anivas
la source
dans mon cas, j'utilisais à l' TaskScheduler.FromCurrentSynchronizationContext()intérieur d'un lambda et l'exécution a été reportée à un autre thread. obtenir le contexte en dehors de lambda a résolu le problème.
M.kazem Akhgary

Réponses:

145

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

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
la source
6
Pour MSTest: mettez le code ci-dessus dans la méthode marquée avec le ClassInitializeAttribute.
Daniel Bişar
6
@SACO: En fait, je dois le mettre dans une méthode avec TestInitializeAttribute, sinon seul le premier test réussit.
Thorarin
2
Pour les tests xunit, je l'ai mis dans le ctor de type statique, car il ne doit être configuré qu'une fois par appareil.
codekaizen
3
Je ne comprends pas du tout pourquoi cette réponse a été acceptée comme solution. ÇA NE MARCHE PAS. Et la raison est simple: SynchronizationContext est une classe factice dont la fonction d'envoi / publication est inutile. Cette classe doit être abstraite plutôt qu'une classe concrète qui peut conduire les gens à une fausse impression de «ça marche». @tofutim Vous souhaitez probablement fournir votre propre implémentation dérivée de SyncContext.
h9uest
1
Je pense que je l'ai compris. Mon TestInitialize est asynchrone. Chaque fois qu'il y a un "wait" dans TestInit, le SynchronizationContext actuel est perdu. C'est parce que (comme @ h9uest l'a souligné), l'implémentation par défaut de SynchronizationContext met simplement les tâches en file d'attente vers ThreadPool et ne continue pas réellement sur le même thread.
Sapph
24

La solution de Ritch Melton n'a pas fonctionné pour moi. C'est parce que ma TestInitializefonction est asynchrone, tout comme mes tests, donc à chaque fois que awaitle courant SynchronizationContextest perdu. Cela est dû au fait que, comme MSDN le souligne, la SynchronizationContextclasse est «stupide» et met tout simplement en file d'attente tous les travaux dans le pool de threads.

Ce qui a fonctionné pour moi, c'est simplement sauter l' FromCurrentSynchronizationContextappel quand il n'y a pas de SynchronizationContext(c'est-à-dire si le contexte actuel est nul ). S'il n'y a pas de thread d'interface utilisateur, je n'ai pas besoin de me synchroniser avec lui 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, qui où:

  • Passer un TaskSchedulerà ViewModel (via l'injection de dépendances)
  • Créez un test SynchronizationContextet un "faux" fil d'interface utilisateur pour les tests à exécuter - beaucoup plus de problèmes pour moi que cela en vaut la peine

Je perds une partie de la nuance de thread, mais je ne teste pas explicitement que mes rappels OnPropertyChanged se déclenchent sur un thread spécifique, donc je suis d'accord avec cela. Les autres réponses utilisant new SynchronizationContext()ne font pas vraiment mieux pour cet objectif de toute façon.

Sapph
la source
Votre elsecas échouera également dans une application de service Windows, ce qui entraînerasyncContextScheduler == null
FindOutIslamNow
Je suis tombé sur le même problème, mais à la place, j'ai lu le code source NUnit. AsyncToSyncAdapter remplace uniquement votre SynchronizationContext s'il s'exécute dans un thread STA. Une solution de contournement consiste à marquer votre classe avec un [RequiresThread]attribut.
Aron
1

J'ai combiné plusieurs solutions pour avoir la garantie de fonctionner 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 => { ... });
}
ujeenator
la source