Come eseguire un metodo Task <T> asincrono in modo sincrono?


628

Sto imparando su asincrono / attendo e mi sono imbattuto in una situazione in cui ho bisogno di chiamare un metodo asincrono in modo sincrono. Come lo posso fare?

Metodo asincrono:

public async Task<Customers> GetCustomers()
{
    return await Service.GetCustomersAsync();
}

Uso normale:

public async void GetCustomers()
{
    customerList = await GetCustomers();
}

Ho provato a utilizzare quanto segue:

Task<Customer> task = GetCustomers();
task.Wait()

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Task<Customer> task = GetCustomers();
while(task.Status != TaskStatus.RanToCompletion)

Ho anche provato un suggerimento da qui , tuttavia non funziona quando il dispatcher è in uno stato sospeso.

public static void WaitWithPumping(this Task task) 
{
        if (task == null) throw new ArgumentNullException(“task”);
        var nestedFrame = new DispatcherFrame();
        task.ContinueWith(_ => nestedFrame.Continue = false);
        Dispatcher.PushFrame(nestedFrame);
        task.Wait();
}

Ecco l'eccezione e la traccia dello stack dalla chiamata RunSynchronously:

System.InvalidOperationException

Messaggio : RunSynchronously non può essere chiamato su un'attività non associata a un delegato.

InnerException : null

Fonte : mscorlib

StackTrace :

          at System.Threading.Tasks.Task.InternalRunSynchronously(TaskScheduler scheduler)
   at System.Threading.Tasks.Task.RunSynchronously()
   at MyApplication.CustomControls.Controls.MyCustomControl.CreateAvailablePanelList() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 638
   at MyApplication.CustomControls.Controls.MyCustomControl.get_AvailablePanels() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 233
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>b__36(DesktopPanel panel) in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 597
   at System.Collections.Generic.List`1.ForEach(Action`1 action)
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>d__3b.MoveNext() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 625
   at System.Runtime.CompilerServices.TaskAwaiter.<>c__DisplayClass7.<TrySetContinuationForAwait>b__1(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at System.Threading.ExecutionContext.runTryCode(Object userData)
   at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at MyApplication.App.Main() in C:\Documents and Settings\...\MyApplication\obj\Debug\App.g.cs:line 50
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

46
La migliore risposta alla domanda "Come posso chiamare un metodo asincrono in modo sincrono" è "non". Ci sono hack per provare a forzarlo a funzionare, ma hanno tutti insidie ​​molto sottili. Invece, esegui il backup e correggi il codice che ti rende "necessario" per farlo.
Stephen Cleary,

58
@Stephen Cleary Assolutamente d'accordo, ma a volte è semplicemente inevitabile, come quando il tuo codice dipende da qualche API di terze parti che non usa asincronizza / attende. Inoltre, se si associa alle proprietà WPF quando si utilizza MVVM, è letteralmente impossibile usare asincroni / wait poiché questo non è supportato sulle proprietà.
Contango,

3
@StephenCleary Non sempre. Sto creando una DLL che verrà importata in GeneXus . Non supporta parole chiave asincrone / wait, quindi devo usare solo metodi sincroni.
Dinei,

5
@StephenCleary 1) GeneXus è uno strumento del terzo pt e non ho accesso al suo codice sorgente; 2) GeneXus non ha nemmeno implementazioni di "funzioni", quindi non riesco a capire come potrei implementare un "callback" con questo tipo di cose. Sicuramente sarebbe una soluzione più difficile che utilizzare in modo Tasksincrono; 3) Sto integrando GeneXus con il driver MongoDB C # , che espone alcuni metodi solo in modo asincrono
Dinei,

1
@ygoe: utilizzare un blocco compatibile asincrono, ad esempio SemaphoreSlim.
Stephen Cleary,

Risposte:


456

Ecco una soluzione alternativa che ho scoperto che funziona per tutti i casi (inclusi i dispatcher sospesi). Non è il mio codice e sto ancora lavorando per comprenderlo appieno, ma funziona.

Può essere chiamato usando:

customerList = AsyncHelpers.RunSync<List<Customer>>(() => GetCustomers());

Il codice è di qui

public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }
}

28
Per alcune informazioni su come funziona, Stephen Toub (Parallel) ha scritto una serie di post su questo. Parte 1 Parte 2 Parte 3
Cameron MacFarland

18
Ho aggiornato il codice di John affinché funzioni senza eseguire il wrapping delle attività in lambdas: github.com/tejacques/AsyncBridge . Fondamentalmente lavori con blocchi asincroni con l'istruzione using. Qualunque cosa all'interno di un blocco utilizzando avviene in modo asincrono, con un'attesa alla fine. Il rovescio della medaglia è che è necessario scartare il compito da soli in una richiamata, ma è comunque abbastanza elegante, soprattutto se è necessario chiamare più funzioni asincrone contemporaneamente.
Tom Jacques,

17
@StephenCleary Anche se generalmente sono d'accordo con te sul fatto che il codice dovrebbe essere asincrono fino in fondo, a volte ti trovi in ​​una situazione impossibile in cui uno deve forzarlo come una chiamata sincrona. Fondamentalmente, la mia situazione è che tutto il mio codice di accesso ai dati è in modo asincrono. Avevo bisogno di creare una Sitemap basata sulla Sitemap e la libreria di terze parti che stavo usando era MvcSitemap. Ora, quando uno lo sta estendendo tramite la DynamicNodeProviderBaseclasse base, non è possibile dichiararlo come asyncmetodo. O ho dovuto sostituire con una nuova libreria o semplicemente chiamare un op sincrono.
justin.lovell,

6
@ justin.lovell: Sì, le limitazioni della libreria possono costringerci a mettere degli hack, almeno fino a quando la libreria non viene aggiornata. Sembra che MvcSitemap sia una di queste situazioni in cui è richiesto un hack (anche filtri MVC e azioni secondarie); Ho persone solo dissuadere da questo, in generale, perché hack come questo sono il modo usato troppo spesso quando sono non necessarie. Con MVC in particolare, alcune API ASP.NET/MVC presumono che ne abbiano una AspNetSynchronizationContext, quindi questo particolare hack non funzionerà se stai chiamando quelle API.
Stephen Cleary,

5
Questo codice non funzionerà. Se viene chiamato da un thread del pool, può attivare il deadlock di thread-fame. Il chiamante bloccherà l'attesa del completamento dell'operazione, cosa che potrebbe non accadere se ha esaurito il pool di thread. Vedere questo articolo .
ZunTzu,

318

Tieni presente che questa risposta ha tre anni. L'ho scritto basandomi principalmente su un'esperienza con .Net 4.0 e molto poco con 4.5 specialmente con async-await. In generale è una bella soluzione semplice, ma a volte rompe le cose. Si prega di leggere la discussione nei commenti.

.Net 4.5

Usa questo:

// For Task<T>: will block until the task is completed...
var result = task.Result; 

// For Task (not Task<T>): will block until the task is completed...
task2.RunSynchronously();

Vedi: TaskAwaiter , Task.Result , Task.RunSincronicamente


.Net 4.0

Usa questo:

var x = (IAsyncResult)task;
task.Start();

x.AsyncWaitHandle.WaitOne();

...o questo:

task.Start();
task.Wait();

67
.Resultpuò produrre un punto morto in alcuni scenari
Jordy Langen il

122
Resultpuò facilmente causare deadlock nel asynccodice , come descrivo sul mio blog.
Stephen Cleary,

8
@StephenCleary Ho letto il tuo post e l'ho provato da solo. Sinceramente penso che qualcuno di microsoft fosse davvero ubriaco ... È lo stesso problema di Winforms e thread di background ....
AK_Oct

9
La domanda riguarda un'attività che viene restituita dal metodo asincrono. Questo tipo di attività potrebbe essere già stata avviata, eseguita o annullata, pertanto l'utilizzo del metodo Task.RunSynchronously può comportare InvalidOperationException . Vedere la pagina MSDN: Metodo Task.RunSynchronously . Inoltre, quell'attività è probabilmente creata dai metodi Task.Factory.StartNew o Task.Run (all'interno del metodo asincrono), quindi è pericoloso provare a riavviarlo. Alcune condizioni di gara possono verificarsi in fase di esecuzione. D' altra parte, Task.Wait e Task.Rultult possono provocare deadlock.
sgnsajgon,

4
Run Synchronous ha funzionato per me ... Non so se mi manca qualcosa, ma questo sembra preferibile agli orrori della risposta contrassegnata - Stavo solo cercando un modo per disattivare l'asincrono per testare il codice che è lì solo per fermarsi l'
interfaccia utente per

121

Sorpreso nessuno ha menzionato questo:

public Task<int> BlahAsync()
{
    // ...
}

int result = BlahAsync().GetAwaiter().GetResult();

Non carino come alcuni degli altri metodi qui, ma ha i seguenti vantaggi:

  • non ingoia eccezioni (come Wait)
  • non avvolgerà alcuna eccezione generata in un AggregateException(mi piace Result)
  • funziona per entrambi Taske Task<T>( provalo tu stesso! )

Inoltre, poiché GetAwaiterè tipizzato in anatra, questo dovrebbe funzionare per qualsiasi oggetto che viene restituito da un metodo asincrono (come ConfiguredAwaitableo YieldAwaitable), non solo Attività.


modifica: Si noti che è possibile per questo approccio (o utilizzo .Result) il deadlock, a meno che non si assicuri di aggiungere .ConfigureAwait(false)ogni volta che si attende, per tutti i metodi asincroni che possono essere raggiunti da BlahAsync()(non solo quelli che chiama direttamente). Spiegazione .

// In BlahAsync() body
await FooAsync(); // BAD!
await FooAsync().ConfigureAwait(false); // Good... but make sure FooAsync() and
                                        // all its descendants use ConfigureAwait(false)
                                        // too. Then you can be sure that
                                        // BlahAsync().GetAwaiter().GetResult()
                                        // won't deadlock.

Se sei troppo pigro per aggiungere .ConfigureAwait(false)ovunque e non ti importa delle prestazioni, puoi farlo in alternativa

Task.Run(() => BlahAsync()).GetAwaiter().GetResult()

1
Funziona per me per cose semplici. Inoltre, se il metodo restituisce un IAsyncOperation, devo prima convertirlo in un'attività: BlahAsync (). AsTask (). GetAwaiter (). GetResult ();
Lee McPherson,

3
Ciò ha causato un deadlock all'interno di un metodo web asmx. Tuttavia, il wrapping della chiamata del metodo in Task.Run () l'ha fatto funzionare: Task.Run (() => BlahAsync ()). GetAwaiter (). GetResult ()
Augusto Barreto

Mi piace questo approccio al meglio sintatticamente perché non coinvolge lambda.
Dythim,

25
Si prega di NON modificare le risposte degli altri per inserire un collegamento al proprio. Se ritieni che la tua risposta sia migliore, lasciala invece come commento.
Rachel

1
docs.microsoft.com/en-us/dotnet/api/… dice GetAwaiter()"Questo metodo è destinato all'utente del compilatore anziché utilizzare direttamente nel codice".
Teofilo

75

È molto più semplice eseguire l'attività sul pool di thread, piuttosto che tentare di ingannare lo scheduler per eseguirlo in modo sincrono. In questo modo puoi essere sicuro che non si bloccherà. Le prestazioni sono influenzate a causa del cambio di contesto.

Task<MyResult> DoSomethingAsync() { ... }

// Starts the asynchronous task on a thread-pool thread.
// Returns a proxy to the original task.
Task<MyResult> task = Task.Run(() => DoSomethingAsync());

// Will block until the task is completed...
MyResult result = task.Result; 

3
Quindi si chiama task.Wait (). Il tipo di dati è semplicemente Task.
Michael L Perry,

1
Supponiamo che DoSomethingAsync () sia un metodo asincrono di lunga durata nel suo insieme (internamente attende un'attività di lunga durata), ma restituisce rapidamente un controllo di flusso al suo chiamante, quindi il lavoro dell'argomento lambda termina anche rapidamente. Il risultato di Tusk.Run () può Task <Task> o Task <Task <>> , quindi stai aspettando un risultato di task esterno che viene completato rapidamente, ma task interno (a causa di un processo di lunga durata nel metodo asincrono) è ancora in esecuzione. Le conclusioni sono che probabilmente dovremo usare l' approccio Unwrap () (come è stato fatto in @ J.Lennon post) per ottenere un comportamento sincrono del metodo asincrono.
sgnsajgon,

5
@sgnsajgon Ti sbagli. Task.Run è diverso da Task.Factory.StartNuovo in quanto scartano automaticamente già il risultato. Vedere questo articolo .
ZunTzu

1
Posso semplicemente scrivere Task.Run(DoSomethingAsync)invece? Ciò rimuove un livello di delegati.
ygoe

1
Sì. Andando nella direzione opposta, però, poiché in Task<MyResult> task = Task.Run(async () => await DoSomethingAsync());è più esplicito e risponde alla preoccupazione di @sgnsajgon che potrebbe restituire un task <Task <MyResult>>. Il corretto sovraccarico di Task.Run è selezionato in entrambi i modi, ma il delegato asincrono rende ovvio il tuo intento.
Michael L Perry,

57

Sto imparando su asincrono / attendo e mi sono imbattuto in una situazione in cui ho bisogno di chiamare un metodo asincrono in modo sincrono. Come lo posso fare?

La risposta migliore è che non lo fai , con i dettagli che dipendono da quale sia la "situazione".

È un getter / setter di proprietà? Nella maggior parte dei casi, è meglio disporre di metodi asincroni rispetto alle "proprietà asincrone". (Per maggiori informazioni, vedi il mio post sul blog sulle proprietà asincrone ).

Si tratta di un'app MVVM e si desidera eseguire l'associazione dati asincrona? Quindi utilizzare qualcosa come il mio NotifyTask, come descritto nel mio articolo MSDN sull'associazione di dati asincroni .

È un costruttore? Quindi probabilmente vorrai considerare un metodo di fabbrica asincrono. (Per maggiori informazioni, vedi il mio post sul blog sui costruttori asincroni ).

C'è quasi sempre una risposta migliore che fare sync-over-async.

Se non è possibile per la tua situazione (e lo sai facendo una domanda qui descrive la situazione ), allora ti consiglio di usare solo il codice sincrono. Async fino in fondo è il migliore; la sincronizzazione fino in fondo è la seconda migliore. La sincronizzazione su asincrono non è consigliata.

Tuttavia, ci sono alcune situazioni in cui è necessario il sync-over-async. In particolare, sei vincolato dal codice chiamante in modo da dover essere sincronizzato (e non hai assolutamente modo di ripensare o riformulare il tuo codice per consentire l'asincronia), e tu hanno a chiamata asincrona del codice. Questo è un situazione molto rara, ma di tanto in tanto si presenta.

In tal caso, dovrai utilizzare uno degli hack descritti nel mio articolo su async sviluppo brownfield , in particolare:

  • Blocco (ad es. GetAwaiter().GetResult() .). Si noti che ciò può causare deadlock (come descrivo sul mio blog).
  • Esecuzione del codice su un thread del pool di thread (ad esempio, Task.Run(..).GetAwaiter().GetResult() ). Si noti che questo funzionerà solo se il codice asincrono può essere eseguito su un thread del pool di thread (ovvero, non dipende da un contesto UI o ASP.NET).
  • Cicli di messaggi nidificati. Si noti che questo funzionerà solo se il codice asincrono assume solo un contesto a thread singolo, non un tipo di contesto specifico (molti UI e codice ASP.NET prevedono un contesto specifico).

I loop di messaggi nidificati sono i più pericolosi di tutti gli hack, perché causano il rientro . Il rientro è estremamente difficile da ragionare e (IMO) è la causa della maggior parte dei bug delle applicazioni su Windows. In particolare, se ti trovi sul thread dell'interfaccia utente e blocchi su una coda di lavoro (in attesa del completamento del lavoro asincrono), il CLR esegue effettivamente alcuni messaggi di pumping per te - gestirà effettivamente alcuni messaggi Win32 dall'interno del tuo codice . Oh, e non hai idea di quali messaggi - quando Chris Brumme dice "Non sarebbe bello sapere esattamente cosa verrà pompato? Sfortunatamente, pompare è un'arte nera che va oltre la comprensione mortale". , quindi non abbiamo davvero alcuna speranza di saperlo.

Quindi, quando blocchi in questo modo su un thread dell'interfaccia utente, stai chiedendo problemi. Un'altra citazione di cbrumme dallo stesso articolo: "Di tanto in tanto, i clienti all'interno o all'esterno dell'azienda scoprono che stiamo pompando messaggi durante il blocco gestito su un STA [thread dell'interfaccia utente]. Questa è una preoccupazione legittima, perché sanno che è molto difficile per scrivere codice robusto di fronte al rientro ".

Sì. Codice molto difficile da scrivere che è robusto di fronte al rientro. E i cicli di messaggi nidificati ti costringono a scrivere codice robusto rispetto al rientro. Questo è il motivo per cui la risposta accettata (e più votata) a questa domanda è estremamente pericolosa nella pratica.

Se sei completamente fuori da tutte le altre opzioni - non puoi riprogettare il tuo codice, non puoi ristrutturarlo in modo che sia asincrono - sei costretto a sincronizzare il codice chiamante immutabile - non puoi cambiare il codice downstream per essere sincronizzato - non puoi bloccare - non puoi eseguire il codice asincrono su un thread separato - quindi e solo allora dovresti considerare di abbracciare il rientro.

Se ti trovi in ​​questo angolo, ti consiglierei di usare qualcosa come Dispatcher.PushFrameper le app WPF , fare un ciclo con Application.DoEventsper le app WinForm e, per il caso generale, il mio AsyncContext.Run.


Stephen, c'è un'altra domanda molto simile a cui hai fornito anche una risposta fantastica. Pensi che uno di essi possa essere chiuso come duplicato o magari unire la richiesta o attivare prima il meta (poiché ogni q ha ~ 200K visualizzazioni oltre 200 voti)? Suggerimenti?
Alexei Levenkov,

1
@AlexeiLevenkov: Non mi sento bene nel farlo, per alcuni motivi: 1) La risposta alla domanda collegata è abbastanza obsoleta. 2) Ho scritto un intero articolo sull'argomento che ritengo più completo di qualsiasi altro Q / A SO esistente. 3) La risposta accettata su questa domanda è estremamente popolare. 4) Sono fortemente contrario a quella risposta accettata. Quindi, chiudendolo come un duplex sarebbe un abuso di potere; chiudendolo come un duplice (o unendo) si potrebbe dare ancora di più una risposta pericolosa. L'ho lasciato e lo lascio alla comunità.
Stephen Cleary,

Ok. Prenderò in considerazione l'idea di farlo su meta che in qualche modo.
Alexei Levenkov,

9
Questa risposta va molto oltre la mia testa. "Usa asincrono fino in fondo" è un consiglio confuso, dato che chiaramente non è possibile seguirlo. Un programma con un Main()metodo asincrono non viene compilato; ad un certo punto hai avuto per colmare il divario tra il mondo di sincronizzazione e asincrone. Non è una " situazione molto rara" , è letteralmente necessaria in ogni programma che chiama un metodo asincrono. Non esiste alcuna opzione per non "eseguire sync-over-async" , solo un'opzione per deviare quel carico fino al metodo di chiamata invece di assumerlo in quello che stai scrivendo.
Mark Amery,

1
Grande. Sto per asyncapplicare tutti i metodi nella mia applicazione ora. E questo è molto. Questo non può essere solo il valore predefinito?
giovedì

25

Se sto leggendo bene la tua domanda, il codice che desidera la chiamata sincrona a un metodo asincrono viene eseguito su un thread dispatcher sospeso. E vuoi effettivamente bloccare in modo sincrono quel thread fino al completamento del metodo asincrono.

I metodi asincroni in C # 5 sono potenziati tagliando in modo efficace il metodo in pezzi sotto il cofano e restituendo uno Taskche può tracciare il completamento complessivo dell'intero shabang. Tuttavia, il modo in cui vengono eseguiti i metodi troncati può dipendere dal tipo di espressione passato aawait all'operatore.

Il più delle volte, utilizzerai awaitun'espressione di tipo Task. L'implementazione del awaitmodello da parte di Task è "intelligente" in quanto si differenzia da SynchronizationContext, il che provoca sostanzialmente quanto segue:

  1. Se il thread entra in await trova su un thread del loop di messaggi Dispatcher o WinForms, garantisce che i blocchi del metodo asincrono si verifichino come parte dell'elaborazione della coda dei messaggi.
  2. Se il thread immesso in si awaittrova su un thread del pool di thread, i blocchi rimanenti del metodo asincrono si verificano in qualsiasi punto del pool di thread.

Ecco perché probabilmente stai riscontrando problemi - l'implementazione del metodo asincrono sta cercando di eseguire il resto sul Dispatcher - anche se è sospeso.

.... backup! ....

Devo porre la domanda, perché stai cercando di bloccare in modo sincrono un metodo asincrono? In questo modo si vanificherebbe lo scopo del motivo per cui il metodo voleva essere chiamato in modo asincrono. In generale, quando si inizia a utilizzare awaitun metodo Dispatcher o UI, è necessario rendere asincrono l'intero flusso dell'interfaccia utente. Ad esempio, se il tuo callstack era simile al seguente:

  1. [Superiore] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing()- WPFoppureWinFormsCodice
  6. [Message Loop] - WPFo WinFormsMessage Loop

Quindi, una volta che il codice è stato trasformato in asincrono, in genere si finisce con

  1. [Superiore] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing()- WPFo WinFormsCodice
  6. [Message Loop] - WPFoppureWinFormsMessage Loop

In realtà rispondendo

La classe AsyncHelpers sopra funziona effettivamente perché si comporta come un ciclo di messaggi nidificato, ma installa il proprio meccanico parallelo sul Dispatcher anziché tentare di eseguirlo sul Dispatcher stesso. Questa è una soluzione alternativa per il tuo problema.

Un'altra soluzione alternativa consiste nell'eseguire il metodo asincrono su un thread di thread pool e quindi attendere il completamento. Farlo è facile: puoi farlo con il seguente frammento:

var customerList = TaskEx.RunEx(GetCustomers).Result;

L'API finale sarà Task.Run (...), ma con il CTP avrai bisogno dei suffissi Ex ( spiegazione qui ).


+1 per la spiegazione dettagliata, tuttavia TaskEx.RunEx(GetCustomers).Resultblocca l'applicazione quando viene eseguita su un thread dispatcher sospeso. Inoltre, il metodo GetCustomers () viene normalmente eseguito in modo asincrono, tuttavia in una situazione deve essere eseguito in modo sincrono, quindi stavo cercando un modo per farlo senza creare una versione di sincronizzazione del metodo.
Rachel

+1 per "perché stai tentando di bloccare in modo sincrono un metodo asincrono?" C'è sempre un modo per usare correttamente i asyncmetodi; i cicli nidificati dovrebbero certamente essere evitati.
Stephen Cleary,

24

Questo funziona bene per me

public static class TaskHelper
{
    public static void RunTaskSynchronously(this Task t)
    {
        var task = Task.Run(async () => await t);
        task.Wait();
    }

    public static T RunTaskSynchronously<T>(this Task<T> t)
    {
        T res = default(T);
        var task = Task.Run(async () => res = await t);
        task.Wait();
        return res;
    }
}

È inoltre necessario utilizzare il metodo Task.Unwrap , poiché l' istruzione Task.Wait provoca l'attesa dell'attività esterna (creata da Task.Run ), non dell'attività di attesa interna passata come parametro del metodo di estensione. Il metodo Task.Run non restituisce l'attività <T>, ma l'attività <Attività <T>>. In alcuni semplici scenari la tua soluzione potrebbe funzionare a causa delle ottimizzazioni di TaskScheduler, ad esempio utilizzando il metodo TryExecuteTaskInline per eseguire attività all'interno del thread corrente durante l' operazione Wait . Per favore, guarda il mio commento a questa risposta.
sgnsajgon,

1
Questo non è corretto Task.Run restituirà Task <T>. Vedi questo sovraccarico msdn.microsoft.com/en-us/library/hh194918(v=vs.110).aspx
Clemente,

Come dovrebbe essere usato? Questo impasse nel WPF:MyAsyncMethod().RunTaskSynchronously();
ygoe,

18

Il modo più semplice che ho trovato per eseguire l'attività in modo sincrono e senza bloccare il thread dell'interfaccia utente è utilizzare RunSynchronously () come:

Task t = new Task(() => 
{ 
   //.... YOUR CODE ....
});
t.RunSynchronously();

Nel mio caso, ho un evento che si attiva quando si verifica qualcosa. Non so quante volte accadrà. Quindi, uso il codice sopra nel mio evento, quindi ogni volta che si attiva, crea un'attività. Le attività vengono eseguite in modo sincrono e per me funziona benissimo. Sono rimasto sorpreso dal fatto che mi ci è voluto così tanto tempo per scoprirlo, considerando quanto sia semplice. Di solito, i consigli sono molto più complessi e soggetti a errori. Questo è stato semplice e pulito.


1
Ma come possiamo usare questo metodo quando il codice asincrono restituisce qualcosa di cui abbiamo bisogno?
S. Serpooshan,

16

L'ho affrontato un paio di volte, principalmente nei test unitari o nello sviluppo di un servizio Windows. Attualmente utilizzo sempre questa funzione:

        var runSync = Task.Factory.StartNew(new Func<Task>(async () =>
        {
            Trace.WriteLine("Task runSync Start");
            await TaskEx.Delay(2000); // Simulates a method that returns a task and
                                      // inside it is possible that there
                                      // async keywords or anothers tasks
            Trace.WriteLine("Task runSync Completed");
        })).Unwrap();
        Trace.WriteLine("Before runSync Wait");
        runSync.Wait();
        Trace.WriteLine("After runSync Waited");

È semplice, facile e non ho avuto problemi.


Questo è l'unico che non si è bloccato per me.
AndreFeijo,

15

Ho trovato questo codice nel componente Microsoft.AspNet.Identity.Core e funziona.

private static readonly TaskFactory _myTaskFactory = new 
     TaskFactory(CancellationToken.None, TaskCreationOptions.None, 
     TaskContinuationOptions.None, TaskScheduler.Default);

// Microsoft.AspNet.Identity.AsyncHelper
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
    CultureInfo cultureUi = CultureInfo.CurrentUICulture;
    CultureInfo culture = CultureInfo.CurrentCulture;
    return AsyncHelper._myTaskFactory.StartNew<Task<TResult>>(delegate
    {
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = cultureUi;
        return func();
    }).Unwrap<TResult>().GetAwaiter().GetResult();
}


13

Solo una piccola nota: questo approccio:

Task<Customer> task = GetCustomers();
task.Wait()

funziona per WinRT.

Lasciatemi spiegare:

private void TestMethod()
{
    Task<Customer> task = GetCustomers(); // call async method as sync and get task as result
    task.Wait(); // wait executing the method
    var customer = task.Result; // get's result.
    Debug.WriteLine(customer.Name); //print customer name
}
public class Customer
{
    public Customer()
    {
        new ManualResetEvent(false).WaitOne(TimeSpan.FromSeconds(5));//wait 5 second (long term operation)
    }
    public string Name { get; set; }
}
private Task<Customer> GetCustomers()
{
    return Task.Run(() => new Customer
    {
        Name = "MyName"
    });
}

Inoltre, questo approccio funziona solo per le soluzioni di Windows Store!

Nota: in questo modo non è sicuro se si chiama il proprio metodo all'interno di un altro metodo asincrono (secondo i commenti di @Servy)


Ho spiegato questa soluzione, controlla la sezione EDIT.
RredCat,

2
Questo può facilmente provocare deadlock quando viene chiamato in situazioni asincrone.
Servito il

@Servy ha un senso. Quindi, come posso ottenere correttamente usando Wait (timeOut) può aiutare, giusto?
RredCat,

1
Quindi devi preoccuparti di raggiungere il timeout quando l'operazione non viene effettivamente eseguita, il che è molto male, e anche il tempo trascorso in attesa fino al timeout nei casi in cui si blocca (e in quel caso stai ancora continuando quando non è finito). Quindi no, questo non risolve il problema.
Servito il

@Servy Sembra che devo implementare la CancellationTokenmia soluzione.
RredCat,

10

Nel tuo codice, attendi prima che venga eseguita l'attività ma non l'hai avviata, quindi attende indefinitamente. Prova questo:

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Modificare:

Dici di avere un'eccezione. Pubblica ulteriori dettagli, inclusa la traccia dello stack.
Mono contiene il seguente caso di test:

[Test]
public void ExecuteSynchronouslyTest ()
{
        var val = 0;
        Task t = new Task (() => { Thread.Sleep (100); val = 1; });
        t.RunSynchronously ();

        Assert.AreEqual (1, val);
}

Controlla se questo funziona per te. In caso contrario, anche se molto improbabile, potresti avere una build strana di CTP asincrono. Se funziona, potresti voler esaminare cosa genera esattamente il compilatore e in che modo l' Taskistanza è diversa da questo esempio.

Modifica n. 2:

Ho verificato con Reflector che l'eccezione che hai descritto si verifica quando lo m_actionè null. Questo è un po 'strano, ma non sono un esperto di CTP asincrono. Come ho detto, si dovrebbe decompilare il codice e vedere come esattamente Taskviene istanziato alcun come mai i suoi m_actionIS null.


PS Qual è il problema con i downvotes occasionali? Ti interessa elaborare?


Ho modificato la mia domanda per rendere il codice che avevo tentato un po 'più chiaro. RunSynchronously restituisce un errore di RunSynchronously may not be called on a task unbound to a delegate. Google non aiuta in quanto tutti i risultati sono in cinese ...
Rachel,

Penso che la differenza sia che non creo l'attività e quindi provo a eseguirla. Al contrario, l'attività viene creata dal metodo asincrono quando awaitviene utilizzata la parola chiave. L'eccezione pubblicata nel mio commento precedente è l'eccezione che ottengo, sebbene sia una delle poche per le quali non riesco a trovare una causa o una soluzione a Google.
Rachel,

1
asynce le asyncparole chiave non sono altro che zucchero di sintassi. Compilatore genera il codice per creare Task<Customer>in GetCustomers()modo che è dove vorrei guardare prima. Per quanto riguarda l'eccezione, hai pubblicato solo un messaggio di eccezione, che è inutile senza tipo di eccezione e traccia stack. Chiama il ToString()metodo dell'eccezione e pubblica l'output nella domanda.
Dan Abramov,

@gaearon: ho pubblicato i dettagli dell'eccezione e la traccia dello stack nella mia domanda originale.
Rachel,

2
@gaearon Penso che tu abbia ottenuto dei voti perché il tuo post non è applicabile alla domanda. La discussione riguarda i metodi async-waitit, non i semplici metodi di restituzione delle attività. Inoltre, secondo me, il meccanismo di attesa asincrona è uno zucchero di sintassi, ma non così banale: c'è continuazione, acquisizione del contesto, ripresa del contesto locale, gestione delle eccezioni locali migliorata e altro ancora. Quindi, non si dovrebbe invocare il metodo RunSynchronously sul risultato del metodo asincrono, perché per definizione il metodo asincrono dovrebbe restituire Task che è attualmente almeno pianificato e più di una volta è nello stato di esecuzione.
sgnsajgon,

9

Testato in .Net 4.6. Può anche evitare il deadlock.

Per il ritorno del metodo asincrono Task.

Task DoSomeWork();
Task.Run(async () => await DoSomeWork()).Wait();

Per il ritorno del metodo asincrono Task<T>

Task<T> GetSomeValue();
var result = Task.Run(() => GetSomeValue()).Result;

Modifica :

Se il chiamante è in esecuzione nel thread del pool di thread (o anche il chiamante è in un'attività), in alcuni casi potrebbe comunque causare un deadlock.


1
La mia risposta dopo quasi 8 anni :) Il secondo esempio - produrrà un deadlock in tutti i contesti pianificati che vengono utilizzati principalmente (app console / .NET core / app desktop / ...). qui hai più visione d'insieme di cosa sto parlando ora: medium.com/rubrikkgroup/…
W92

Resultè perfetto per il lavoro se si desidera una chiamata sincrona, e assolutamente pericoloso altrimenti. Non c'è nulla nel nome Resulto nell'intelletto di Resultciò che indica che è una chiamata bloccante. Dovrebbe davvero essere rinominato.
Zodman,

5

usa il codice di seguito riportato

Task.WaitAll(Task.Run(async () => await service.myAsyncMethod()));

4

Perché non creare una chiamata come:

Service.GetCustomers();

non è asincrono.


4
Sarà quello che faccio se non riesco a farlo funzionare ... creare una versione di sincronizzazione oltre a una versione asincrona
Rachel

3

Questa risposta è progettata per chiunque stia utilizzando WPF per .NET 4.5.

Se si tenta di eseguire Task.Run()sul thread della GUI, si task.Wait()bloccherà indefinitamente, se non si dispone della asyncparola chiave nella definizione della funzione.

Questo metodo di estensione risolve il problema controllando se siamo sul thread GUI e, in tal caso, eseguendo l'attività sul thread dispatcher WPF.

Questa classe può fungere da collante tra il mondo asincrono / attende e il mondo non asincrono / attende, in situazioni in cui è inevitabile, come le proprietà MVVM o le dipendenze da altre API che non usano asincrono / attendono.

/// <summary>
///     Intent: runs an async/await task synchronously. Designed for use with WPF.
///     Normally, under WPF, if task.Wait() is executed on the GUI thread without async
///     in the function signature, it will hang with a threading deadlock, this class 
///     solves that problem.
/// </summary>
public static class TaskHelper
{
    public static void MyRunTaskSynchronously(this Task task)
    {
        if (MyIfWpfDispatcherThread)
        {
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E99213. Task did not run to completion.");
            }
        }
        else
        {
            task.Wait();
            if (task.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E33213. Task did not run to completion.");
            }
        }
    }

    public static T MyRunTaskSynchronously<T>(this Task<T> task)
    {       
        if (MyIfWpfDispatcherThread)
        {
            T res = default(T);
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { res = await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E89213. Task did not run to completion.");
            }
            return res;
        }
        else
        {
            T res = default(T);
            var result = Task.Run(async () => res = await task);
            result.Wait();
            if (result.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E12823. Task did not run to completion.");
            }
            return res;
        }
    }

    /// <summary>
    ///     If the task is running on the WPF dispatcher thread.
    /// </summary>
    public static bool MyIfWpfDispatcherThread
    {
        get
        {
            return Application.Current.Dispatcher.CheckAccess();
        }
    }
}

3

Chiamare semplicemente .Result;o .Wait()è un rischio di deadlock come molti hanno detto nei commenti. Dal momento che molti di noi amano gli oneliner, puoi usarli per.Net 4.5<

Acquisizione di un valore tramite un metodo asincrono:

var result = Task.Run(() => asyncGetValue()).Result;

Chiamare in modo sincrono un metodo asincrono

Task.Run(() => asyncMethod()).Wait();

Non si verificheranno problemi di deadlock dovuti all'uso di Task.Run.

Fonte:

https://stackoverflow.com/a/32429753/3850405


1

Penso che anche il seguente metodo di supporto possa risolvere il problema.

private TResult InvokeAsyncFuncSynchronously<TResult>(Func< Task<TResult>> func)
    {
        TResult result = default(TResult);
        var autoResetEvent = new AutoResetEvent(false);

        Task.Run(async () =>
        {
            try
            {
                result = await func();
            }
            catch (Exception exc)
            {
                mErrorLogger.LogError(exc.ToString());
            }
            finally
            {
                autoResetEvent.Set();
            }
        });
        autoResetEvent.WaitOne();

        return result;
    }

Può essere utilizzato nel modo seguente:

InvokeAsyncFuncSynchronously(Service.GetCustomersAsync);

1
Spiega la votazione
donttellya,

2
... Sono ancora molto interessato perché questa risposta è stata votata in negativo?
Donttellya,

Non è un vero "sincrono". Devi creare due thread e attendere i primi risultati dell'altro.
tmt

e a parte tutto, questa è una pessima idea.
Dan Pantry,

1
Ho appena scritto quasi lo stesso codice (riga per riga la stessa) ma invece usando SemaphoreSlim invece dell'evento di ripristino automatico. Vorrei averlo visto prima. Trovo questo approccio per prevenire deadlock e mantiene il codice asincrono in esecuzione come in scenari asincroni reali. Non sono proprio sicuro del perché questa sia una cattiva idea. Sembra molto più pulito rispetto agli altri approcci che ho visto sopra.
tmrog,

0

Questo funziona per me

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        public static void RunSync(Func<Task> func)
        {
            _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }

        public static TResult RunSync<TResult>(Func<Task<TResult>> func)
        {
            return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }
    }

    class SomeClass
    {
        public async Task<object> LoginAsync(object loginInfo)
        {
            return await Task.FromResult(0);
        }
        public object Login(object loginInfo)
        {
            return AsyncHelper.RunSync(() => LoginAsync(loginInfo));
            //return this.LoginAsync(loginInfo).Result.Content;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var someClass = new SomeClass();

            Console.WriteLine(someClass.Login(1));
            Console.ReadLine();
        }
    }
}

-1

Ho scoperto che SpinWait funziona abbastanza bene per questo.

var task = Task.Run(()=>DoSomethingAsyncronous());

if(!SpinWait.SpinUntil(()=>task.IsComplete, TimeSpan.FromSeconds(30)))
{//Task didn't complete within 30 seconds, fail...
   return false;
}

return true;

L'approccio di cui sopra non ha bisogno di usare .Result o .Wait (). Ti consente anche di specificare un timeout in modo da non rimanere bloccato per sempre nel caso in cui l'attività non venga mai completata.


1
Il downvote suggerisce che a qualcuno non piace questo metodo. C'è qualcuno che può commentare il rovescio della medaglia di questo?
Grax32

In assenza del downvoter che dice PERCHÉ è stato dato il downvote, qualcuno può votarlo? :-)
Curtis,

1
Questo è il polling (spinning), il delegato prenderà il thread dal pool fino a 1000 volte al secondo. Potrebbe non restituire il controllo immediatamente dopo il completamento dell'attività ( errore fino a 10 + ms ). Se terminato dal timeout, l'attività continuerà a essere eseguita, il che rende il timeout praticamente inutile.
Sinatr,

In realtà, lo sto usando dappertutto nel mio codice e quando la condizione è soddisfatta, SpinWaitSpinUntil () esce immediatamente. Quindi, a seconda di quale condizione si verifichi per prima, "condizione soddisfatta" o timeout, l'attività termina. Non continua a funzionare.
Curtis,

-3

Su wp8:

Avvolgilo:

Task GetCustomersSynchronously()
{
    Task t = new Task(async () =>
    {
        myCustomers = await GetCustomers();
    }
    t.RunSynchronously();
}

Chiamalo:

GetCustomersSynchronously();

3
No, questo non funzionerà, perché l'attività non attende il delegato dal costruttore (è un delegato e non un compito ..)
Rico Suter,

-4
    private int GetSync()
    {
        try
        {
            ManualResetEvent mre = new ManualResetEvent(false);
            int result = null;

            Parallel.Invoke(async () =>
            {
                result = await SomeCalcAsync(5+5);
                mre.Set();
            });

            mre.WaitOne();
            return result;
        }
        catch (Exception)
        {
            return null;
        }
    }

-5

Oppure potresti semplicemente andare con:

customerList = Task.Run<List<Customer>>(() => { return GetCustomers(); }).Result;

Per compilare questo accertarsi di fare riferimento al gruppo di estensioni:

System.Net.Http.Formatting

-9

Prova il seguente codice funziona per me:

public async void TaskSearchOnTaskList (SearchModel searchModel)
{
    try
    {
        List<EventsTasksModel> taskSearchList = await Task.Run(
            () => MakeasyncSearchRequest(searchModel),
            cancelTaskSearchToken.Token);

        if (cancelTaskSearchToken.IsCancellationRequested
                || string.IsNullOrEmpty(rid_agendaview_search_eventsbox.Text))
        {
            return;
        }

        if (taskSearchList == null || taskSearchList[0].result == Constants.ZERO)
        {
            RunOnUiThread(() => {
                textViewNoMembers.Visibility = ViewStates.Visible;                  
                taskListView.Visibility = ViewStates.Gone;
            });

            taskSearchRecureList = null;

            return;
        }
        else
        {
            taskSearchRecureList = TaskFooterServiceLayer
                                       .GetRecurringEvent(taskSearchList);

            this.SetOnAdapter(taskSearchRecureList);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("ActivityTaskFooter -> TaskSearchOnTaskList:" + ex.Message);
    }
}
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.