Come chiamare il metodo asincrono dal metodo sincrono in C#?
-
27-10-2019 - |
Domanda
Ho un public async void Foo()
Metodo che voglio chiamare dal metodo sincrono. Finora tutto ciò che ho visto dalla documentazione di MSDN è chiamare i metodi Async tramite metodi Async, ma il mio intero programma non è creato con metodi Async.
È anche possibile?
Ecco un esempio di chiamare questi metodi da un metodo asincrono: http://msdn.microsoft.com/en-us/library/hh300224(v=vs.110).aspx
Ora sto cercando di chiamare questi metodi asincrici dai metodi di sincronizzazione.
Soluzione
La programmazione asincrona "cresce" attraverso la base di codice. È stato Rispetto a un virus zombi. La soluzione migliore è permettergli di crescere, ma a volte non è possibile.
Ho scritto alcuni tipi nel mio Nito.Asyncex Libreria per trattare con una base di codice parzialmente asincrona. Tuttavia, non c'è soluzione che funzioni in ogni situazione.
Soluzione A.
Se hai un semplice metodo asincrono che non è necessario sincronizzare nel suo contesto, allora puoi usare Task.WaitAndUnwrapException
:
var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();
Tu fai non voglio usare Task.Wait
o Task.Result
Perché avvolgono le eccezioni in AggregateException
.
Questa soluzione è appropriata solo se MyAsyncMethod
non si sincronizza al suo contesto. In altre parole, ogni await
in MyAsyncMethod
dovrebbe finire con ConfigureAwait(false)
. Ciò significa che non può aggiornare alcun elemento dell'interfaccia utente o accedere al contesto della richiesta ASP.NET.
Soluzione b
Se MyAsyncMethod
Deve sincronizzarsi al suo contesto, allora potresti essere in grado di utilizzare AsyncContext.RunTask
Per fornire un contesto nidificato:
var result = AsyncContext.RunTask(MyAsyncMethod).Result;
*Aggiornamento 14/04/2014: nelle versioni più recenti della biblioteca l'API è la seguente:
var result = AsyncContext.Run(MyAsyncMethod);
(Va bene usare Task.Result
In questo esempio perché RunTask
si propagerà Task
eccezioni).
Il motivo per cui potresti aver bisogno AsyncContext.RunTask
invece di Task.WaitAndUnwrapException
è a causa di una possibilità piuttosto sottile che si verifica su Winforms/WPF/SL/ASP.NET:
- Un metodo sincrono chiama un metodo asincrone, ottenendo un file
Task
. - Il metodo sincrono fa un'attesa bloccante su
Task
. - Il
async
usi del metodoawait
senzaConfigureAwait
. - Il
Task
non può completare in questa situazione perché completa solo quando ilasync
Il metodo è finito; ilasync
Il metodo non può completare perché sta tentando di programmare la sua continuazione aSynchronizationContext
, e Winforms/WPF/SL/ASP.NET non consentirà l'esecuzione della continuazione perché il metodo sincrono è già in esecuzione in quel contesto.
Questo è uno dei motivi per cui è una buona idea da usare ConfigureAwait(false)
all'interno di ogni async
metodo il più possibile.
Soluzione c
AsyncContext.RunTask
Non funzionerà in ogni scenario. Ad esempio, se il async
Il metodo attende qualcosa che richiede un evento dell'interfaccia utente da completare, quindi avrai il punto anche con il contesto nidificato. In tal caso, potresti iniziare il async
Metodo sul pool di thread:
var task = Task.Run(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();
Tuttavia, questa soluzione richiede un file MyAsyncMethod
Funzionerà nel contesto del pool di thread. Quindi non può aggiornare gli elementi dell'interfaccia utente o accedere al contesto della richiesta ASP.NET. E in tal caso, puoi anche aggiungere ConfigureAwait(false)
al suo await
dichiarazioni e usa la soluzione A.
AGGIORNAMENTO, 2019-05-01: Le attuali "pratiche di meno attesa" sono in un Articolo MSDN qui.
Altri suggerimenti
L'aggiunta di una soluzione che ha finalmente risolto il mio problema, si spera che risparmia il tempo di qualcuno.
Innanzitutto leggi un paio di articoli di Stephen Cleary:
Dalle "due migliori pratiche" in "non bloccare il codice asincrico", il primo non ha funzionato per me e il secondo non era applicabile (in sostanza se posso usare await
, Io faccio!).
Quindi ecco la mia soluzione alternativa: avvolgi la chiamata all'interno di Task.Run<>(async () => await FunctionAsync());
E si spera che no Deadlock più.
Ecco il mio codice:
public class LogReader
{
ILogger _logger;
public LogReader(ILogger logger)
{
_logger = logger;
}
public LogEntity GetLog()
{
Task<LogEntity> task = Task.Run<LogEntity>(async () => await GetLogAsync());
return task.Result;
}
public async Task<LogEntity> GetLogAsync()
{
var result = await _logger.GetAsync();
// more code here...
return result as LogEntity;
}
}
Microsoft ha creato una classe Asynchelper (interna) per eseguire Async come sincronizzazione. La fonte sembra:
internal static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return AsyncHelper._myTaskFactory
.StartNew<Task<TResult>>(func)
.Unwrap<TResult>()
.GetAwaiter()
.GetResult();
}
public static void RunSync(Func<Task> func)
{
AsyncHelper._myTaskFactory
.StartNew<Task>(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
Le classi di base di Microsoft.aspnet.Identity hanno solo metodi asincrici e per chiamarli come sincronizzazione ci sono classi con metodi di estensione che sembrano (utilizzo di esempio):
public static TUser FindById<TUser, TKey>(this UserManager<TUser, TKey> manager, TKey userId) where TUser : class, IUser<TKey> where TKey : IEquatable<TKey>
{
if (manager == null)
{
throw new ArgumentNullException("manager");
}
return AsyncHelper.RunSync<TUser>(() => manager.FindByIdAsync(userId));
}
public static bool IsInRole<TUser, TKey>(this UserManager<TUser, TKey> manager, TKey userId, string role) where TUser : class, IUser<TKey> where TKey : IEquatable<TKey>
{
if (manager == null)
{
throw new ArgumentNullException("manager");
}
return AsyncHelper.RunSync<bool>(() => manager.IsInRoleAsync(userId, role));
}
Per coloro che sono preoccupati per i termini di licenza del codice, ecco un collegamento a un codice molto simile (aggiunge solo supporto per la cultura sul thread) che ha commenti per indicare che è autorizzato a Microsoft. https://github.com/aspnet/aspne identity/blob/master/src/microsoft.aspnet.identity.core/asynchelper.cs
Async Main fa ora parte di C# 7.2 e può essere abilitato nelle impostazioni di build avanzate di progetti.
Per C# <7.2, il modo corretto è:
static void Main(string[] args)
{
MainAsync().GetAwaiter().GetResult();
}
static async Task MainAsync()
{
/*await stuff here*/
}
Vedrai questo utilizzato in molta documentazione Microsoft, ad esempio:https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subsscriptions
public async Task<string> StartMyTask()
{
await Foo()
// code to execute once foo is done
}
static void Main()
{
var myTask = StartMyTask(); // call your method which will return control once it hits await
// now you can continue executing code here
string result = myTask.Result; // wait for the task to complete to continue
// use result
}
Leggi la parola chiave "Aspetta" come "Avvia questa attività a lungo termine, quindi restituisci il controllo al metodo chiamante". Una volta eseguita l'attività di lunga data, quindi esegue il codice dopo di esso. Il codice dopo l'attesa è simile a quelli che erano metodi di callback. La grande differenza è che il flusso logico non viene interrotto, il che rende molto più facile scrivere e leggere.
Non sono sicuro al 100%, ma credo che la tecnica descritta in Questo blog dovrebbe funzionare in molte circostanze:
Puoi quindi usare
task.GetAwaiter().GetResult()
Se si desidera invocare direttamente questa logica di propagazione.
La risposta più accettata non è del tutto corretta. Esiste una soluzione che funziona in ogni situazione: una pompa di messaggi ad hoc (SynchronizationContext).
Il thread chiamante verrà bloccato come previsto, pur garantendo che tutte le continue chiamate dalla funzione asincrima non bloccano in quanto verranno riuniti alla sincronizzazione ad hoc (pompa di messaggi) in esecuzione sul thread chiamante.
Il codice del supporto per pompa del messaggio ad hoc:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Threading
{
/// <summary>Provides a pump that supports running asynchronous methods on the current thread.</summary>
public static class AsyncPump
{
/// <summary>Runs the specified asynchronous method.</summary>
/// <param name="asyncMethod">The asynchronous method to execute.</param>
public static void Run(Action asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(true);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function
syncCtx.OperationStarted();
asyncMethod();
syncCtx.OperationCompleted();
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// <summary>Runs the specified asynchronous method.</summary>
/// <param name="asyncMethod">The asynchronous method to execute.</param>
public static void Run(Func<Task> asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(false);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = asyncMethod();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// <summary>Runs the specified asynchronous method.</summary>
/// <param name="asyncMethod">The asynchronous method to execute.</param>
public static T Run<T>(Func<Task<T>> asyncMethod)
{
if (asyncMethod == null) throw new ArgumentNullException("asyncMethod");
var prevCtx = SynchronizationContext.Current;
try
{
// Establish the new context
var syncCtx = new SingleThreadSynchronizationContext(false);
SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completes
var t = asyncMethod();
if (t == null) throw new InvalidOperationException("No task provided.");
t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptions
syncCtx.RunOnCurrentThread();
return t.GetAwaiter().GetResult();
}
finally { SynchronizationContext.SetSynchronizationContext(prevCtx); }
}
/// <summary>Provides a SynchronizationContext that's single-threaded.</summary>
private sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
/// <summary>The queue of work items.</summary>
private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> m_queue =
new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();
/// <summary>The processing thread.</summary>
private readonly Thread m_thread = Thread.CurrentThread;
/// <summary>The number of outstanding operations.</summary>
private int m_operationCount = 0;
/// <summary>Whether to track operations m_operationCount.</summary>
private readonly bool m_trackOperations;
/// <summary>Initializes the context.</summary>
/// <param name="trackOperations">Whether to track operation count.</param>
internal SingleThreadSynchronizationContext(bool trackOperations)
{
m_trackOperations = trackOperations;
}
/// <summary>Dispatches an asynchronous message to the synchronization context.</summary>
/// <param name="d">The System.Threading.SendOrPostCallback delegate to call.</param>
/// <param name="state">The object passed to the delegate.</param>
public override void Post(SendOrPostCallback d, object state)
{
if (d == null) throw new ArgumentNullException("d");
m_queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));
}
/// <summary>Not supported.</summary>
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronously sending is not supported.");
}
/// <summary>Runs an loop to process all queued work items.</summary>
public void RunOnCurrentThread()
{
foreach (var workItem in m_queue.GetConsumingEnumerable())
workItem.Key(workItem.Value);
}
/// <summary>Notifies the context that no more work will arrive.</summary>
public void Complete() { m_queue.CompleteAdding(); }
/// <summary>Invoked when an async operation is started.</summary>
public override void OperationStarted()
{
if (m_trackOperations)
Interlocked.Increment(ref m_operationCount);
}
/// <summary>Invoked when an async operation is completed.</summary>
public override void OperationCompleted()
{
if (m_trackOperations &&
Interlocked.Decrement(ref m_operationCount) == 0)
Complete();
}
}
}
}
Uso:
AsyncPump.Run(() => FooAsync(...));
È disponibile una descrizione più dettagliata della pompa asincrimale qui.
A chiunque prestasse più attenzione a questa domanda ...
Se guardi dentro Microsoft.VisualStudio.Services.WebApi
C'è una classe chiamata TaskExtensions
. All'interno di quella classe vedrai il metodo di estensione statica Task.SyncResult()
, che piace totalmente blocca il thread fino a quando l'attività non ritorna.
Internamente chiama task.GetAwaiter().GetResult()
Il che è piuttosto semplice, tuttavia è sovraccarico di lavorare su qualsiasi async
Metodo che ritorna Task
, Task<T>
o Task<HttpResponseMessage>
... zucchero sintattico, piccola ... papà ha un debole per i dolci.
Sembra ...GetAwaiter().GetResult()
è il modo di MS-official di eseguire il codice Async in un contesto di blocco. Sembra funzionare molto bene per il mio caso d'uso.
var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);
OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();
O usa questo:
var result=result.GetAwaiter().GetResult().AccessToken
Puoi chiamare qualsiasi metodo asincrono dal codice sincrono, cioè fino a quando non è necessario await
su di loro, nel qual caso devono essere contrassegnati async
anche.
Come molte persone stanno suggerendo qui, potresti chiamare Wait () o derivare dall'attività risultante nel tuo metodo sincrono, ma poi si finisce con una chiamata di blocco in quel metodo, che in qualche modo sconfigge lo scopo dell'asincronizzazione.
Non puoi davvero creare il tuo metodo async
E non vuoi bloccare il metodo sincrono, quindi dovrai utilizzare un metodo di callback passandolo come parametro al metodo Continuewith sull'attività.
So di essere così tardi. Ma nel caso in cui qualcuno come me volesse risolverlo in modo pulito e facile e senza dipendere da un'altra biblioteca.
Ho trovato quanto segue pezzo di codice da Ryan
public static class AsyncHelpers
{
private static readonly TaskFactory taskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
/// <summary>
/// Executes an async Task method which has a void return value synchronously
/// USAGE: AsyncUtil.RunSync(() => AsyncMethod());
/// </summary>
/// <param name="task">Task method to execute</param>
public static void RunSync(Func<Task> task)
=> taskFactory
.StartNew(task)
.Unwrap()
.GetAwaiter()
.GetResult();
/// <summary>
/// Executes an async Task<T> method which has a T return type synchronously
/// USAGE: T result = AsyncUtil.RunSync(() => AsyncMethod<T>());
/// </summary>
/// <typeparam name="TResult">Return Type</typeparam>
/// <param name="task">Task<T> method to execute</param>
/// <returns></returns>
public static TResult RunSync<TResult>(Func<Task<TResult>> task)
=> taskFactory
.StartNew(task)
.Unwrap()
.GetAwaiter()
.GetResult();
}
allora puoi chiamarlo così
var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());
Se vuoi eseguirlo sincronizza
MethodAsync().RunSynchronously()
Quei metodi di asincroni di Windows hanno un piccolo metodo elegante chiamato Astask (). Puoi usarlo per avere il metodo restituire da solo come attività in modo da poter chiamare manualmente Wait () su di esso.
Ad esempio, su un'applicazione Silverlight di Windows Phone 8, puoi fare quanto segue:
private void DeleteSynchronous(string path)
{
StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
Task t = localFolder.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask();
t.Wait();
}
private void FunctionThatNeedsToBeSynchronous()
{
// Do some work here
// ....
// Delete something in storage synchronously
DeleteSynchronous("pathGoesHere");
// Do other work here
// .....
}
Spero che sia di aiuto!
//Example from non UI thread -
private void SaveAssetAsDraft()
{
SaveAssetDataAsDraft();
}
private async Task<bool> SaveAssetDataAsDraft()
{
var id = await _assetServiceManager.SavePendingAssetAsDraft();
return true;
}
//UI Thread -
var result = Task.Run(() => SaveAssetDataAsDraft().Result).Result;