730 votes

Comment exécuter une méthode asynchrone Task<T> de manière synchrone ?

Je suis en train de me familiariser avec async/await, et je me suis retrouvé dans une situation où je dois appeler une méthode async de manière synchrone. Comment puis-je le faire ?

Méthode asynchrone :

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

Utilisation normale :

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

J'ai essayé d'utiliser ce qui suit :

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

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

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

J'ai également essayé une suggestion de ici Cependant, cela ne fonctionne pas lorsque le répartiteur est en état de suspension.

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

Voici l'exception et le suivi de pile de l'appel RunSynchronously :

System.InvalidOperationException

Message : RunSynchronously ne peut pas être appelé sur une tâche non liée à un délégué.

InnerException : null

Source : : 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()

51 votes

La meilleure réponse à la question "Comment puis-je appeler une méthode asynchrone de manière synchrone" est "ne le faites pas". Il existe hacks pour essayer de le forcer à fonctionner, mais ils ont tous des pièges très subtils. Au lieu de cela, faites marche arrière et corrigez le code qui vous rend "nécessaire" de faire cela.

83 votes

Stephen Cleary est tout à fait d'accord, mais parfois c'est tout simplement inévitable, par exemple lorsque votre code dépend d'une API tierce qui n'utilise pas async/await. En outre, si vous liez des propriétés WPF en utilisant MVVM, il est littéralement impossible d'utiliser async/await car cela n'est pas pris en charge par les propriétés.

5 votes

@StephenCleary Pas toujours. Je suis en train de construire une DLL qui sera importée en GeneXus . Il ne supporte pas les mots-clés async/await, je dois donc utiliser uniquement des méthodes synchrones.

524voto

Rachel Points 49408

Voici une solution de contournement que j'ai trouvée et qui fonctionne dans tous les cas (y compris pour les répartiteurs suspendus). Ce n'est pas mon code et je travaille encore à le comprendre complètement, mais cela fonctionne.

On peut l'appeler en utilisant :

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

Le code provient de ici

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

29 votes

Pour savoir comment cela fonctionne, Stephen Toub (M. Parallel) a écrit une série de billets à ce sujet. Partie 1 Partie 2 Partie 3

18 votes

J'ai mis à jour le code de John pour qu'il fonctionne sans envelopper les tâches dans des lambdas : github.com/tejacques/AsyncBridge . Essentiellement, vous travaillez avec des blocs asynchrones avec l'instruction using. Tout ce qui se trouve à l'intérieur d'un bloc using se déroule de manière asynchrone, avec une attente à la fin. L'inconvénient est que vous devez déballer la tâche vous-même dans un callback, mais cela reste assez élégant, surtout si vous devez appeler plusieurs fonctions asynchrones à la fois.

1 votes

Cette méthode bloque le thread de l'interface utilisateur sur lequel elle est exécutée. Serait-il possible d'utiliser Dispatcher.PushFrame(DispatcherFrame) pour éviter de bloquer l'interface utilisateur ?

357voto

AK_ Points 2169

Sachez que cette réponse date d'il y a trois ans. Je l'ai écrite en me basant principalement sur une expérience avec .Net 4.0, et très peu avec 4.5, surtout avec async-await . En règle générale, il s'agit d'une solution simple et agréable, mais elle peut parfois entraîner des problèmes. Veuillez lire la discussion dans les commentaires.

.Net 4.5

Utilisez juste ça :

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

Voir : TaskAwaiter , Résultat de la tâche , Tâche.ExécuterSynchroniquement


.Net 4.0

Utilisez ça :

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

x.AsyncWaitHandle.WaitOne();

...ou ceci :

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

0 votes

Si vous souhaitez utiliser async dans .NET 4.0, vous pouvez installer le paquet NuGet async : nuget.org/packages/Microsoft.Bcl.Async

75 votes

.Result peut produire un blocage dans certains scénarios.

0 votes

@JordyLangen bien sûr, vous attendez que cette tâche se termine... Mais c'est habituel avec la programmation parallèle, et toujours vrai quand on attend quelque chose...

79voto

Michael L Perry Points 2380

Il est beaucoup plus simple d'exécuter la tâche sur le pool de threads, plutôt que d'essayer de tromper le planificateur pour l'exécuter de manière synchrone. De cette façon, vous pouvez être sûr qu'il n'y aura pas de blocage. Les performances sont affectées par le changement de contexte.

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;

0 votes

Et si la tâche est nulle (sans résultat) ?

3 votes

Ensuite, vous appelez task.Wait(). Le type de données est simplement Task.

1 votes

Supposons que DoSomethingAsync() est une méthode asynchrone de longue durée dans son ensemble (en interne, elle attend une tâche de longue durée), mais elle rend rapidement un contrôle de flux à son appelant, ce qui fait que le travail de l'argument lambda se termine aussi rapidement. Le résultat de Tusk.Run() peut être Tâche<Tâche> o Tâche<Tâche<>> Vous attendez donc le résultat d'une tâche externe qui se termine rapidement, mais la tâche interne (en raison de l'attente d'un travail de longue durée dans la méthode asynchrone) est toujours en cours. Les conclusions sont que nous devons probablement utiliser Unwrap() (comme cela a été fait dans le post de @J.Lennon) pour obtenir un comportement synchrone de la méthode asynchrone.

26voto

Theo Yaung Points 1593

Si je lis bien votre question - le code qui veut l'appel synchrone à une méthode asynchrone s'exécute sur un thread de distributeur suspendu. Et vous voulez réellement faire un appel synchrone bloc ce thread jusqu'à ce que la méthode asynchrone soit terminée.

Les méthodes asynchrones en C# 5 sont activées en découpant effectivement la méthode en morceaux sous le capot, et en renvoyant une méthode de type Task qui peut suivre l'achèvement global de l'ensemble du shabang. Cependant, la façon dont les méthodes découpées s'exécutent peut dépendre du type de l'expression passée à la fonction await opérateur.

La plupart du temps, vous utiliserez await sur une expression de type Task . La mise en œuvre par Task de la await est "intelligent" en ce sens qu'il s'en remet à l'approche de la SynchronizationContext ce qui a pour effet de provoquer ce qui suit :

  1. Si le fil qui entre dans le await se trouve sur un Dispatcher ou un thread de boucle de messages WinForms, il garantit que les morceaux de la méthode asynchrone se produisent dans le cadre du traitement de la file d'attente des messages.
  2. Si le fil qui entre dans le await se trouve sur un thread du pool de threads, alors les autres morceaux de la méthode asynchrone se produisent n'importe où dans le pool de threads.

C'est la raison pour laquelle vous rencontrez probablement des problèmes - l'implémentation de la méthode asynchrone essaie d'exécuter le reste sur le Dispatcher, même s'il est suspendu.

.... reculer ! ....

Je dois poser la question, pourquoi Essayez-vous de bloquer de manière synchrone une méthode asynchrone ? Cela irait à l'encontre de la raison pour laquelle la méthode doit être appelée de manière asynchrone. En général, lorsque vous commencez à utiliser await sur un répartiteur ou une méthode d'interface utilisateur, vous voudrez rendre asynchrone l'ensemble de votre flux d'interface utilisateur. Par exemple, si votre callstack était quelque chose comme le suivant :

  1. [Haut] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing() - WPF o WinForms Code
  6. [Message Loop] - WPF o WinForms Boucle de messages

Ensuite, une fois que le code a été transformé pour utiliser l'asynchronisme, vous vous retrouverez typiquement avec

  1. [Haut] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing() - WPF o WinForms Code
  6. [Message Loop] - WPF o WinForms Boucle de messages

Réponse effective

La classe AsyncHelpers ci-dessus fonctionne en fait parce qu'elle se comporte comme une boucle de messages imbriquée, mais elle installe sa propre mécanique parallèle au répartiteur plutôt que d'essayer d'exécuter sur le répartiteur lui-même. C'est une solution de contournement pour votre problème.

Une autre solution consiste à exécuter votre méthode asynchrone sur un thread du pool de threads, puis à attendre qu'elle se termine. C'est facile à faire - vous pouvez le faire avec le snippet suivant :

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

L'API finale sera Task.Run(...), mais avec le CTP vous aurez besoin des suffixes Ex ( explication ici ).

0 votes

+1 pour l'explication détaillée, cependant TaskEx.RunEx(GetCustomers).Result suspend l'application lorsqu'elle est exécutée sur un thread de répartiteur suspendu. De plus, la méthode GetCustomers() est normalement exécutée de manière asynchrone, mais dans une situation donnée, elle doit être exécutée de manière synchrone. Je cherchais donc un moyen de le faire sans construire une version synchrone de la méthode.

0 votes

+1 pour "pourquoi essayez-vous de bloquer de manière synchrone une méthode asynchrone ?" Il y a toujours un moyen d'utiliser correctement async Les boucles imbriquées sont à éviter.

17voto

J. Lennon Points 974

J'y ai été confronté à quelques reprises, principalement dans le cadre de tests unitaires ou du développement d'un service Windows. Actuellement, j'utilise toujours cette fonctionnalité :

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

C'est simple, facile et je n'ai eu aucun problème.

0 votes

C'est le seul qui n'a pas fait d'impasse pour moi.

0 votes

Je ne sais pas ce que c'est, mais c'est essentiellement Task.Run(() => ..).Wait() (avec de légères modifications). Les deux devraient fonctionner.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X