web-dev-qa-db-fra.com

Comment appeler une méthode asynchrone à partir d'une méthode synchrone en C #?

J'ai une méthode public async void Foo() que je souhaite appeler depuis une méthode synchrone. Jusqu'à présent, tout ce que j'ai vu dans la documentation MSDN appelle des méthodes asynchrones via des méthodes asynchrones, mais l'ensemble de mon programme n'est pas construit avec des méthodes asynchrones.

Est-ce seulement possible?

Voici un exemple d'appel de ces méthodes à partir d'une méthode asynchrone: http://msdn.Microsoft.com/en-us/library/hh300224 (v = vs.110) .aspx

Maintenant, je cherche à appeler ces méthodes asynchrones à partir de méthodes de synchronisation.

687
Tower

La programmation asynchrone "grandit" dans la base de code. Cela a été comparé à un virus zombie . La meilleure solution consiste à lui permettre de croître, mais parfois ce n'est pas possible.

J'ai écrit quelques types dans ma bibliothèque Nito.AsyncEx pour traiter avec une base de code partiellement asynchrone. Cependant, aucune solution ne fonctionne dans toutes les situations.

Solution A

Si vous avez une méthode asynchrone simple qui n'a pas besoin de se synchroniser sur son contexte, vous pouvez utiliser Task.WaitAndUnwrapException:

var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();

Vous ne voulez pas utiliser Task.Wait ou Task.Result car ils enveloppent les exceptions dans AggregateException.

Cette solution n'est appropriée que si MyAsyncMethod ne se synchronise pas dans son contexte. En d'autres termes, chaque await dans MyAsyncMethod devrait se terminer par ConfigureAwait(false). Cela signifie qu'il ne peut mettre à jour aucun élément de l'interface utilisateur ni accéder au contexte de la demande ASP.NET.

Solution B

Si MyAsyncMethod doit être synchronisé avec son contexte, vous pourrez alors utiliser AsyncContext.RunTask pour fournir un contexte imbriqué:

var result = AsyncContext.RunTask(MyAsyncMethod).Result;

* Mise à jour 14/04/2014: dans les versions plus récentes de la bibliothèque, l'API est la suivante:

var result = AsyncContext.Run(MyAsyncMethod);

(Il est correct d'utiliser Task.Result dans cet exemple car RunTask propagera les exceptions Task.).

La raison pour laquelle vous pouvez avoir besoin de AsyncContext.RunTask au lieu de Task.WaitAndUnwrapException est à cause d'une possibilité de blocage assez subtile qui se produit sur WinForms/WPF/SL/ASP.NET:

  1. Une méthode synchrone appelle une méthode asynchrone, obtenant un Task.
  2. La méthode synchrone effectue une attente de blocage sur le Task.
  3. La méthode async utilise await sans ConfigureAwait.
  4. La Task ne peut pas se terminer dans cette situation car elle ne se termine que lorsque la méthode async est terminée; la méthode async ne peut pas se terminer car elle tente de planifier sa continuation vers SynchronizationContext, et WinForms/WPF/SL/ASP.NET n'autorisera pas l'exécution de la suite car la méthode synchrone est déjà en cours d'exécution dans ce contexte.

C'est l'une des raisons pour lesquelles il est judicieux d'utiliser autant que possible ConfigureAwait(false) dans chaque méthode async.

Solution C

AsyncContext.RunTask ne fonctionnera pas dans tous les scénarios. Par exemple, si la méthode async attend quelque chose qui nécessite la réalisation d'un événement d'interface utilisateur, vous obtiendrez un blocage même avec le contexte imbriqué. Dans ce cas, vous pouvez démarrer la méthode async sur le pool de threads:

var task = Task.Run(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();

Cependant, cette solution nécessite un MyAsyncMethod qui fonctionnera dans le contexte du pool de threads. Il ne peut donc pas mettre à jour les éléments de l'interface utilisateur ni accéder au contexte de la demande ASP.NET. Et dans ce cas, vous pouvez également ajouter ConfigureAwait(false) à ses instructions await et utiliser la solution A.

Update, 2019-05-01: Les "pratiques les moins pires" actuelles figurent dans un article MSDN ici .

588
Stephen Cleary

Ajouter une solution qui a finalement résolu mon problème permet, espérons-le, de faire gagner du temps à quelqu'un.

Tout d’abord, lisez quelques articles de Stephen Cleary :

Parmi les "deux meilleures pratiques" de "Ne pas bloquer sur du code asynchrone", la première ne me convenait pas et la seconde ne s'appliquait pas (si je peux utiliser await, c'est ce que je fais! ).

Voici donc ma solution de contournement: insérer l’appel dans une Task.Run<>(async () => await FunctionAsync()); et, espérons-le, plus .

Voici mon code:

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

Microsoft a créé une classe AsyncHelper (interne) pour exécuter Async en tant que Sync. La source ressemble à:

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

Les classes de base Microsoft.AspNet.Identity ont uniquement des méthodes Async. Pour les appeler en tant que Sync, il existe des classes avec des méthodes d'extension qui ressemblent à (exemple d'utilisation):

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

Pour ceux qui s'inquiètent des conditions de licence du code, voici un lien vers un code très similaire (ajoute simplement le support de la culture sur le thread) qui contient des commentaires indiquant qu'il s'agit de MIT sous licence de Microsoft. https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

182
Erik Philips

async Main fait maintenant partie de C # 7.2 et peut être activé dans les paramètres de construction avancés du projet.

Pour C # <7.2, la manière correcte est:

static void Main(string[] args)
{
   MainAsync().GetAwaiter().GetResult();
}


static async Task MainAsync()
{
   /*await stuff here*/
}

Vous verrez ceci utilisé dans de nombreux documents Microsoft, par exemple: https://docs.Microsoft.com/en-us/Azure/service-bus-messaging/service-bus-dotnet-how- utiliser-sujets-abonnements

120
Lee Smith
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

}

Vous lisez le mot-clé 'wait' sous la forme "lancez cette tâche longue, puis remettez le contrôle à la méthode d'appel". Une fois la tâche longue exécutée, le code est exécuté après. Le code après l'attente est similaire à ce qui était auparavant des méthodes CallBack. La grande différence étant que le flux logique n'est pas interrompu, ce qui facilite grandement l'écriture et la lecture.

48
Despertar

Je ne suis pas sûr à 100%, mais je crois que la technique décrite dans ce blog devrait fonctionner dans de nombreuses circonstances:

Vous pouvez donc utiliser task.GetAwaiter().GetResult() si vous souhaitez appeler directement cette logique de propagation.

31
NStuke

La réponse la plus acceptée n'est pas tout à fait correcte. Il IS une solution qui fonctionne dans toutes les situations: une pompe de messagerie ad-hoc (SynchronizationContext).

Le thread appelant sera bloqué comme prévu, tout en veillant à ce que toutes les continuations appelées à partir de la fonction asynchrone ne se bloquent pas car elles seront marshalées vers le SynchronizationContext (pompe de message) ad-hoc s'exécutant sur le thread appelant.

Le code de l’assistant de pompe de message 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();
            }
        }
    }
}

Usage:

AsyncPump.Run(() => FooAsync(...));

Une description plus détaillée de la pompe asynchrone est disponible ici .

23
Robert J

À quiconque fait plus attention à cette question ...

Si vous regardez dans Microsoft.VisualStudio.Services.WebApi il y a une classe appelée TaskExtensions. Dans cette classe, vous verrez la méthode d'extension statique Task.SyncResult(), qui bloque totalement le thread jusqu'au retour de la tâche.

En interne, il appelle task.GetAwaiter().GetResult() qui est assez simple, mais il est surchargé de fonctionner sur n’importe quelle méthode async qui renvoie Task, Task<T> ou Task<HttpResponseMessage>... sucre syntaxique , bébé ... papa a la dent sucrée.

Il semble que ...GetAwaiter().GetResult() soit la manière officielle de MS d’exécuter du code asynchrone dans un contexte bloquant. Semble fonctionner très bien pour mon cas d'utilisation.

7
jrypkahauer

Vous pouvez appeler n’importe quelle méthode asynchrone à partir de code synchrone, c’est-à-dire jusqu’à ce que vous ayez besoin de await, auquel cas elles doivent également être marquées async.

Comme beaucoup de gens le suggèrent ici, vous pouvez appeler Wait () ou Result sur la tâche résultante dans votre méthode synchrone, mais vous vous retrouvez avec un appel bloquant dans cette méthode, ce qui annule en quelque sorte la fonction asynchrone.

Si vous ne pouvez vraiment pas créer votre méthode async et que vous ne voulez pas verrouiller la méthode synchrone, vous devrez alors utiliser une méthode de rappel en la passant comme paramètre à la méthode ContinueWith de la tâche. .

3
base2

Je sais que je suis si tard. Mais au cas où quelqu'un comme moi voudrait résoudre ce problème de manière simple, nette et sans dépendre d'une autre bibliothèque.

J'ai trouvé ce qui suit morceau de code de 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();
}

alors vous pouvez l'appeler comme ça

var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());
3
Wahid Bitar
var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);

OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();

Ou utilisez ceci:

var result=result.GetAwaiter().GetResult().AccessToken
2
rajesh A