Bieżący SynchronizationContext nie może być używany jako TaskScheduler

98

Używam zadań do uruchamiania długotrwałych wywołań serwera w moim ViewModel, a wyniki są kierowane z powrotem przy Dispatcherużyciu TaskScheduler.FromSyncronizationContext(). Na przykład:

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

Działa to dobrze, gdy uruchamiam aplikację. Ale kiedy uruchamiam NUnittesty Resharper, otrzymuję komunikat o błędzie w połączeniu z FromCurrentSynchronizationContext:

Bieżący SynchronizationContext nie może być używany jako TaskScheduler.

Wydaje mi się, że dzieje się tak, ponieważ testy są uruchamiane na wątkach roboczych. Jak mogę upewnić się, że testy są uruchamiane w głównym wątku? Wszelkie inne sugestie są mile widziane.

anivas
źródło
w moim przypadku używałem TaskScheduler.FromCurrentSynchronizationContext()wewnątrz lambdy i wykonanie zostało odłożone na inny wątek. pobranie kontekstu poza lambdę rozwiązało problem.
M.kazem Akhgary

Odpowiedzi:

145

Musisz podać SynchronizationContext. Oto jak sobie z tym radzę:

[SetUp]
public void TestSetUp()
{
  SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
Ritch Melton
źródło
6
Dla MSTest: umieść powyższy kod w metodzie oznaczonej atrybutem ClassInitializeAttribute.
Daniel Bişar
6
@SACO: Właściwie, muszę to umieścić w metodzie z TestInitializeAttribute, w przeciwnym razie tylko pierwszy test przechodzi.
Thorarin
2
W przypadku testów xunit umieściłem go w module ctor typu statycznego, ponieważ wystarczy go skonfigurować tylko raz na urządzenie.
codekaizen
3
W ogóle nie rozumiem, dlaczego ta odpowiedź została przyjęta jako rozwiązanie. TO NIE DZIAŁA. Powód jest prosty: SynchronizationContext to fikcyjna klasa, której funkcja send / post jest bezużyteczna. Ta klasa powinna być raczej abstrakcyjna niż konkretna, która prawdopodobnie prowadzi ludzi do fałszywego poczucia „to działa”. @tofutim Prawdopodobnie chcesz zapewnić własną implementację pochodzącą z SyncContext.
h9uest
1
Myślę, że to rozgryzłem. Mój TestInitialize jest asynchroniczny. Za każdym razem, gdy w TestInit występuje „await”, bieżący SynchronizationContext jest tracony. Dzieje się tak, ponieważ (jak wskazał @ h9uest), domyślna implementacja SynchronizationContext po prostu kolejkuje zadania do puli wątków i w rzeczywistości nie jest kontynuowana w tym samym wątku.
Sapph
24

Rozwiązanie Ritcha Meltona nie działało dla mnie. Dzieje się tak, ponieważ moja TestInitializefunkcja jest asynchroniczna, podobnie jak moje testy, więc z każdym awaitprądem SynchronizationContextjest tracony. Dzieje się tak, ponieważ, jak wskazuje MSDN, SynchronizationContextklasa jest „głupia” i po prostu kolejkuje całą pracę do puli wątków.

To, co mi zadziałało, to po prostu pomijanie FromCurrentSynchronizationContextwywołania, gdy nie ma znaku SynchronizationContext(to znaczy, jeśli bieżący kontekst ma wartość null ). Jeśli nie ma wątku interfejsu użytkownika, w pierwszej kolejności nie muszę się z nim synchronizować.

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

To rozwiązanie okazało się prostsze niż alternatywy, w których:

  • Przekaż TaskSchedulerdo ViewModel (przez iniekcję zależności)
  • Utwórz test SynchronizationContexti „fałszywy” wątek interfejsu użytkownika, aby testy działały dalej - dla mnie o wiele więcej kłopotu, niż jest to warte

Tracę część niuansów wątków, ale nie testuję jawnie, czy moje wywołania zwrotne OnPropertyChanged wyzwalają w określonym wątku, więc nie przeszkadza mi to. Inne odpowiedzi new SynchronizationContext()nie są tak naprawdę lepsze dla tego celu.

Safa
źródło
Twoja elsesprawa zakończy się niepowodzeniem również w aplikacji usługi systemu Windows, w wyniku czegosyncContextScheduler == null
FindOutIslamNow
Napotkałem ten sam problem, ale zamiast tego przeczytałem kod źródłowy NUnit. AsyncToSyncAdapter zastępuje tylko SynchronizationContext, jeśli jest uruchomiony w wątku STA. Sposób obejścia problemu polega na oznaczeniu klasy [RequiresThread]atrybutem.
Aron
1

Połączyłem wiele rozwiązań, aby mieć gwarancję działania 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;
    }
}

Stosowanie:

var context = CustomSynchronizationContext.GetSynchronizationContext();

if (context != null) 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... }, context);
}
else 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... });
}
ujeenator
źródło