Der aktuelle SynchronizationContext darf nicht als TaskScheduler verwendet werden

98

Ich verwende Aufgaben , um lange laufende Serveraufrufe in meinem ViewModel auszuführen, und die Ergebnisse werden bei DispatcherVerwendung wieder zusammengeführt TaskScheduler.FromSyncronizationContext(). Beispielsweise:

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

Dies funktioniert gut, wenn ich die Anwendung ausführe. Wenn ich meine NUnitTests jedoch ausführe, wird Resharperbeim Aufruf die folgende Fehlermeldung angezeigt FromCurrentSynchronizationContext:

Der aktuelle SynchronizationContext darf nicht als TaskScheduler verwendet werden.

Ich denke, das liegt daran, dass die Tests auf Arbeitsthreads ausgeführt werden. Wie kann ich sicherstellen, dass die Tests im Hauptthread ausgeführt werden? Alle anderen Vorschläge sind willkommen.

Anivas
quelle
In meinem Fall habe ich TaskScheduler.FromCurrentSynchronizationContext()in einem Lambda verwendet und die Ausführung wurde auf einen anderen Thread verschoben. Das Problem wurde behoben, indem der Kontext außerhalb von Lambda abgerufen wurde.
M. Kazem Akhgary 27.

Antworten:

145

Sie müssen einen SynchronizationContext angeben. So gehe ich damit um:

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
quelle
6
Für MSTest: Fügen Sie den obigen Code in die mit ClassInitializeAttribute gekennzeichnete Methode ein.
Daniel Bişar
6
@SACO: Eigentlich muss ich es in eine Methode mit setzen TestInitializeAttribute, sonst besteht nur der erste Test.
Thorarin
2
Für xunit-Tests habe ich es in den statischen Typ ctor eingefügt, da es nur einmal pro Gerät eingerichtet werden muss.
Codekaizen
3
Ich verstehe überhaupt nicht, warum diese Antwort als Lösung akzeptiert wurde. ES FUNKTIONIERT NICHT. Und der Grund ist einfach: SynchronizationContext ist eine Dummy-Klasse, deren Sende- / Post-Funktion unbrauchbar ist. Diese Klasse sollte eher abstrakt als eine konkrete Klasse sein, die Menschen möglicherweise in ein falsches Gefühl von "es funktioniert" führt. @tofutim Sie möchten wahrscheinlich Ihre eigene Implementierung bereitstellen, die von SyncContext abgeleitet ist.
h9uest
1
Ich glaube, ich habe es herausgefunden. Mein TestInitialize ist asynchron. Jedes Mal, wenn im TestInit ein "Warten" erfolgt, geht der aktuelle SynchronizationContext verloren. Dies liegt daran, dass (wie @ h9uest hervorhob) die Standardimplementierung von SynchronizationContext nur Aufgaben in den ThreadPool einreiht und nicht im selben Thread fortgesetzt wird.
Sapph
24

Die Lösung von Ritch Melton hat bei mir nicht funktioniert. Dies liegt daran, dass meine TestInitializeFunktion asynchron ist, ebenso wie meine Tests, sodass bei jedem awaitder Strom SynchronizationContextverloren geht. Dies liegt daran, dass, wie MSDN hervorhebt, dieSynchronizationContext Klasse, "dumm" ist und nur alle Arbeiten an den Thread-Pool in die Warteschlange stellt.

Was für mich funktioniert hat, ist, den FromCurrentSynchronizationContextAnruf einfach zu überspringen , wenn es keinen gibt SynchronizationContext(dh wenn der aktuelle Kontext null ist ). Wenn es keinen UI-Thread gibt, muss ich ihn überhaupt nicht synchronisieren.

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

Ich fand diese Lösung einfacher als die Alternativen, bei denen:

  • Pass a TaskScheduler an das ViewModel (über Abhängigkeitsinjektion)
  • Erstellen Sie einen Test SynchronizationContextund einen "gefälschten" UI-Thread, damit die Tests ausgeführt werden können - viel mehr Ärger für mich, als es wert ist

Ich verliere einen Teil der Threading-Nuance, teste jedoch nicht explizit, ob meine OnPropertyChanged-Rückrufe für einen bestimmten Thread ausgelöst werden, daher bin ich damit einverstanden. Die anderen Antworten, die mit new SynchronizationContext()verwendet werden, sind für dieses Ziel sowieso nicht wirklich besser.

Sapph
quelle
Ihr elseFall wird auch in einer Windows-Service-App fehlschlagen, was dazu führtsyncContextScheduler == null
FindOutIslamNow
Kam auf das gleiche Problem, aber stattdessen las ich den NUnit-Quellcode. AsyncToSyncAdapter überschreibt Ihren SynchronizationContext nur, wenn er in einem STA-Thread ausgeführt wird. Eine Problemumgehung besteht darin, Ihre Klasse mit einem [RequiresThread]Attribut zu markieren .
Aron
1

Ich habe mehrere Lösungen kombiniert, um die Funktionsfähigkeit von SynchronizationContext zu gewährleisten:

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

Verwendung:

var context = CustomSynchronizationContext.GetSynchronizationContext();

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