L'attuale SynchronizationContext non può essere utilizzato come TaskScheduler


98

Sto usando Tasks per eseguire chiamate server a lunga esecuzione nel mio ViewModel e i risultati vengono reimpostati durante l' Dispatcherutilizzo TaskScheduler.FromSyncronizationContext(). Per esempio:

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

Funziona bene quando eseguo l'applicazione. Ma quando eseguo i miei NUnittest Resharperottengo il messaggio di errore sulla chiamata a FromCurrentSynchronizationContextcome:

L'attuale SynchronizationContext non può essere utilizzato come TaskScheduler.

Immagino che ciò sia dovuto al fatto che i test vengono eseguiti sui thread di lavoro. Come posso assicurarmi che i test vengano eseguiti sul thread principale? Eventuali altri suggerimenti sono i benvenuti.


nel mio caso stavo usando TaskScheduler.FromCurrentSynchronizationContext()all'interno di un lambda e l'esecuzione è stata rimandata a un altro thread. ottenere il contesto esterno a lambda ha risolto il problema.
M.kazem Akhgary

Risposte:


145

È necessario fornire un SynchronizationContext. Ecco come lo gestisco:

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

6
Per MSTest: inserire il codice sopra nel metodo contrassegnato con ClassInitializeAttribute.
Daniel Bişar

6
@SACO: In realtà, devo metterlo in un metodo con TestInitializeAttribute, altrimenti solo il primo test viene superato.
Thorarin

2
Per i test xunit, l'ho inserito nel ctor di tipo statico, poiché deve essere impostato solo una volta per dispositivo.
codekaizen

3
Non capisco affatto perché questa risposta sia stata accettata come soluzione. NON FUNZIONA. E il motivo è semplice: SynchronizationContext è una classe fittizia le cui funzioni di invio / invio sono inutili. Questa classe dovrebbe essere astratta piuttosto che una classe concreta che potrebbe portare le persone a una falsa sensazione di "funziona". @tofutim Probabilmente vorrai fornire la tua implementazione derivata da SyncContext.
h9uest

1
Penso di aver capito. Il mio TestInitialize è asincrono. Ogni volta che è presente un "wait" in TestInit, il SynchronizationContext corrente viene perso. Questo perché (come ha sottolineato @ h9uest), l'implementazione predefinita di SynchronizationContext accoda semplicemente le attività in ThreadPool e non continua effettivamente sullo stesso thread.
Sapph

24

La soluzione di Ritch Melton non ha funzionato per me. Questo perché la mia TestInitializefunzione è asincrona, così come i miei test, quindi con ogni awaitcorrente SynchronizationContextviene persa. Questo perché, come sottolinea MSDN, la SynchronizationContextclasse è "stupida" e accoda tutto il lavoro nel pool di thread.

Quello che ha funzionato per me in realtà è solo saltare la FromCurrentSynchronizationContextchiamata quando non c'è un SynchronizationContext(cioè, se il contesto corrente è nullo ). Se non è presente alcun thread dell'interfaccia utente, non è necessario sincronizzarsi con esso in primo luogo.

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

Ho trovato questa soluzione più semplice delle alternative, che dove:

  • Passa TaskSchedulera al ViewModel (tramite inserimento delle dipendenze)
  • Crea un test SynchronizationContexte un thread "falso" dell'interfaccia utente affinché i test vengano eseguiti - molti più problemi per me che ne valgono la pena

Perdo alcune sfumature di threading, ma non sto testando esplicitamente che i miei callback OnPropertyChanged si attivino su un thread specifico, quindi mi va bene. Le altre risposte che usano new SynchronizationContext()non fanno davvero meglio per quell'obiettivo comunque.


Il tuo elsecaso fallirà anche in un'app di servizio Windows, risultandosyncContextScheduler == null
FindOutIslamNow

Mi sono imbattuto nello stesso problema, ma invece ho letto il codice sorgente di NUnit. AsyncToSyncAdapter sostituisce solo SynchronizationContext se è in esecuzione in un thread STA. Una soluzione alternativa è contrassegnare la classe con un [RequiresThread]attributo.
Aron

1

Ho combinato più soluzioni per avere la garanzia per il funzionamento di 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;
    }
}

Utilizzo:

var context = CustomSynchronizationContext.GetSynchronizationContext();

if (context != null) 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... }, context);
}
else 
{
    Task.Factory
      .StartNew(() => { ... })
      .ContinueWith(x => { ... });
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.