Question Comment appeler la méthode asynchrone de la méthode synchrone en C #?


j'ai un public async void Foo() méthode que je veux appeler à partir de la méthode synchrone. Jusqu'à présent, tout ce que j'ai vu à partir de la documentation MSDN appelle des méthodes asynchrones via des méthodes asynchrones, mais mon programme entier 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 des méthodes de synchronisation.


480
2018-02-18 17:49


origine


Réponses:


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

J'ai écrit quelques types dans mon Nito.AsyncEx bibliothèque pour traiter une base de code partiellement asynchrone. Cependant, il n'y a pas de solution qui fonctionne dans toutes les situations.

Solution A

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

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

Tu fais ne pas vouloir utiliser Task.Wait ou Task.Result parce qu'ils enveloppent des exceptions dans AggregateException.

Cette solution ne convient que si MyAsyncMethod ne se synchronise pas avec son contexte. En d'autres termes, tous les await dans MyAsyncMethod devrait se terminer par ConfigureAwait(false). Cela signifie qu'il ne peut pas mettre à jour les éléments d'interface utilisateur ou accéder au contexte de demande ASP.NET.

Solution B

Si MyAsyncMethod a besoin de se synchroniser à son contexte, alors vous pouvez être en mesure d'utiliser AsyncContext.RunTask 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);

(C'est OK d'utiliser Task.Result dans cet exemple parce que RunTask va se propager Task des exceptions).

La raison pour laquelle vous pourriez avoir besoin AsyncContext.RunTask au lieu de Task.WaitAndUnwrapException est à cause d'une possibilité d'interruption plutôt 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 fait une attente de blocage sur le Task.
  3. le async utilisations de la méthode await sans pour autant ConfigureAwait.
  4. le Task ne peut pas terminer dans cette situation, car il ne se termine que lorsque le async la méthode est terminée; la async méthode ne peut pas terminer car elle tente de planifier sa poursuite à la SynchronizationContextet WinForms / WPF / SL / ASP.NET ne permettent pas l'exécution de la continuation car la méthode synchrone est déjà en cours d'exécution dans ce contexte.

C'est une des raisons pour lesquelles c'est une bonne idée d'utiliser ConfigureAwait(false) dans chaque async méthode autant que possible.

Solution C

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

var task = TaskEx.RunEx(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();

Cependant, cette solution nécessite un MyAsyncMethod cela fonctionnera dans le contexte du pool de threads. Il ne peut donc pas mettre à jour les éléments de l'interface utilisateur ou accéder au contexte de la requête ASP.NET. Et dans ce cas, vous pouvez aussi bien ajouter ConfigureAwait(false)à son await déclarations et utiliser la solution A.


421
2018-02-18 18:06



Microsoft a construit 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 et 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 sont préoccupés par les termes du code de licence, voici un lien vers un code très similaire (ajoute juste le support de la culture sur le thread) qui contient des commentaires indiquant qu'il s'agit de MIT sous licence par Microsoft. https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs


139
2017-08-02 17:10



Ajout d'une solution qui a finalement résolu mon problème, espérons sauver le temps de quelqu'un.

Tout d'abord lire quelques articles de Stephen Cleary:

À partir des "deux meilleures pratiques" de "Ne pas bloquer le code asynchrone", la première ne fonctionnait pas pour moi et la seconde ne s'appliquait pas (si je pouvais utiliser await, Je fais!).

Donc, voici ma solution de contournement: enveloppez l'appel dans un Task.Run<>(async () => await FunctionAsync()); et j'espère que non impasse 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;
    }
}

122
2018-02-24 15:43



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

Pour C # <7.2, la bonne façon est:

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


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

57
2018-02-18 17:55



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' en tant que "lancez cette longue tâche, puis renvoyez le contrôle à la méthode d'appel". Une fois la tâche longue terminée, le code est exécuté. Après l'attente, le code est similaire à celui des méthodes CallBack. La grande différence étant que le flux logique n'est pas interrompu, ce qui le rend beaucoup plus facile à écrire et à lire.


43
2018-03-26 22:14



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 voulez appeler directement cette logique de propagation.


24
2017-08-25 07:28



La réponse la plus acceptée n'est pas tout à fait correcte. Il existe une solution qui fonctionne dans toutes les situations: une pompe à messages 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 ad hoc (pompe de message) s'exécutant sur le thread appelant.

Le code de l'assistant de pompage 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.


17
2018-02-18 17:54