Как мне запустить синхронный асинхронный метод Task <T>? - PullRequest
589 голосов
/ 23 февраля 2011

Я изучаю async / await и столкнулся с ситуацией, когда мне нужно синхронно вызывать асинхронный метод. Как я могу это сделать?

Асинхронный метод:

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

Нормальное использование:

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

Я пытался использовать следующее:

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

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

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

Я также попробовал предложение из здесь , однако оно не работает, когда диспетчер находится в приостановленном состоянии.

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

Вот исключение и трассировка стека от вызова RunSynchronously:

System.InvalidOperationException

Сообщение : RunSynchronously не может быть вызван для задачи, не связанной с делегатом.

InnerException : ноль

Источник : 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()

Ответы [ 24 ]

427 голосов
/ 24 февраля 2011

Вот обходной путь, который я нашел, который работает для всех случаев (включая приостановленных диспетчеров). Это не мой код, и я до сих пор работаю над его полным пониманием, но он работает.

Его можно вызвать с помощью:

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

Код от здесь

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;
        }
    }
}
309 голосов
/ 02 августа 2012

Имейте в виду этому ответу три года.Я написал это, основываясь в основном на опыте с .Net 4.0, и очень мало на 4.5, особенно на async-await.Вообще говоря, это хорошее простое решение, но оно иногда ломает вещи.Пожалуйста, прочитайте обсуждение в комментариях.

.Net 4.5

Просто используйте это:

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

См .: TaskAwaiter , Task.Результат , Task.RunSynchronously


.Net 4.0

Используйте это:

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

x.AsyncWaitHandle.WaitOne();

... или это:

task.Start();
task.Wait();
110 голосов
/ 07 февраля 2016

Удивлен, никто не упомянул это:

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

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

Не так красиво, как некоторые другие методы здесь, но имеет следующие преимущества:

Кроме того, поскольку GetAwaiter типизирован по типу утки, это должно работать для любого объекта, возвращаемого из асинхронного метода (например, ConfiguredAwaitable или YieldAwaitable), а не только для задач.


edit: Обратите внимание, что этот подход (или использование .Result) может привести к взаимоблокировке, если только вы не добавите .ConfigureAwait(false) каждый раз, когда ожидаете, для всех асинхронных методов, которые могут быть достигнутым от BlahAsync() (не только те, которые он вызывает напрямую). Объяснение .

// 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.

Если вам лень добавлять .ConfigureAwait(false) везде, и вам не важна производительность, вы можете альтернативно сделать

Task.Run(() => BlahAsync()).GetAwaiter().GetResult()
73 голосов
/ 13 июня 2013

Гораздо проще запустить задачу в пуле потоков, чем пытаться обмануть планировщик, чтобы запустить ее синхронно. Таким образом, вы можете быть уверены, что это не тупик. На производительность влияет изменение контекста.

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; 
50 голосов
/ 07 сентября 2016

Я изучаю async / await и столкнулся с ситуацией, когда мне нужно синхронно вызывать асинхронный метод. Как я могу это сделать?

Лучший ответ - Вы не , детали зависят от ситуации.

Является ли это свойство геттером / сеттером? В большинстве случаев лучше иметь асинхронные методы, чем "асинхронные свойства". (Для получения дополнительной информации см. мой пост в блоге об асинхронных свойствах ).

Это приложение MVVM, и вы хотите выполнить асинхронное связывание данных? Затем используйте что-то вроде моего NotifyTask, как описано в моей статье MSDN об асинхронном связывании данных .

Это конструктор? Тогда вы, вероятно, захотите рассмотреть асинхронный фабричный метод. (Подробнее см. В моем блоге об асинхронных конструкторах ).

Почти всегда лучший ответ, чем синхронизация по асинхронности.

Если это невозможно для вашей ситуации (и вы знаете это, задавая вопрос здесь , описывающий ситуацию ), то я бы рекомендовал просто использовать синхронный код. Асинхронный полностью лучше; Синхронизация полностью на втором месте. Синхронизация по асинхронности не рекомендуется.

Однако есть несколько ситуаций, когда необходима синхронизация по асинхронности. В частности, вы ограничены вызывающим кодом, так что у вас есть для синхронизации (и у вас нет абсолютно никакого способа переосмыслить или реструктурировать ваш код для обеспечения асинхронности), и у вас есть для вызова асинхронного кода. Это очень редкая ситуация, но она возникает время от времени.

В этом случае вам понадобится один из хаков, описанных в моей статье о brownfield async development , а именно:

  • Блокировка (например, GetAwaiter().GetResult()). Обратите внимание, что это может вызвать взаимные блокировки (как я описал в моем блоге).
  • Запуск кода в потоке пула потоков (например, Task.Run(..).GetAwaiter().GetResult()). Обратите внимание, что это будет работать только в том случае, если асинхронный код может выполняться в потоке пула потоков (т. Е. Не зависит от контекста пользовательского интерфейса или ASP.NET).
  • Вложенные циклы сообщений. Обратите внимание, что это будет работать только в том случае, если асинхронный код принимает только однопоточный контекст, а не определенный тип контекста (большая часть кода пользовательского интерфейса и ASP.NET ожидает определенного контекста).

Вложенные циклы сообщений являются наиболее опасными из всех хаков, поскольку они вызывают повторный вход . Повторный вход чрезвычайно сложен, и (IMO) является причиной большинства ошибок приложений в Windows. В частности, если вы находитесь в потоке пользовательского интерфейса и блокируете рабочую очередь (ожидая завершения асинхронной работы), то CLR фактически выполняет для вас некоторую перекачку сообщений - он фактически обрабатывает некоторые сообщения Win32 из вашего кода . О, и вы не знаете, какие сообщения - когда Крис Брамм говорит: «Разве не было бы замечательно точно знать, что будет накачано? К сожалению, накачка - это черное искусство, которое находится за пределами понимания смертных. ", тогда у нас действительно нет надежды узнать.

Итак, когда вы блокируете таким образом в потоке пользовательского интерфейса, у вас возникают проблемы. Другая цитата cbrumme из той же статьи: «Время от времени клиенты внутри или за пределами компании обнаруживают, что мы перекачиваем сообщения во время управляемой блокировки в STA [поток пользовательского интерфейса]. Это законное беспокойство, потому что они знают, что это очень сложно написать код, устойчивый к повторному входу. "

Да, это так. Очень трудный для написания код, устойчивый к повторному входу. И вложенные циклы сообщений заставляют писать код, устойчивый к повторному входу. Вот почему принятый (и наиболее одобренный) ответ на этот вопрос чрезвычайно опасен на практике.

Если у вас полностью отсутствуют другие возможности - вы не можете изменить свой код, вы не можете реструктурировать его так, чтобы он был асинхронным - вас заставляет неизменный вызывающий код синхронизировать - вы не можете изменить нижестоящий код для синхронизации- вы не можете заблокировать - вы не можете запустить асинхронный код в отдельном потоке - тогда и только тогда , если вы решите принять повторный вход.

Если вы окажетесь в этом углуЯ бы порекомендовал использовать что-то вроде Dispatcher.PushFrame для приложений WPF , цикл с Application.DoEvents для приложений WinForm, а для общего случая мой собственный AsyncContext.Run.

23 голосов
/ 23 июля 2014

Это хорошо работает для меня

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;
    }
}
22 голосов
/ 25 февраля 2011

Если я правильно понял ваш вопрос - код, который хочет синхронный вызов асинхронного метода, выполняется в приостановленном потоке диспетчера. И вы хотите фактически синхронно блокировать этот поток, пока не будет завершен асинхронный метод.

Асинхронные методы в C # 5 приводятся в действие, эффективно разбивая метод на части под капотом, и возвращая Task, который может отслеживать общее завершение всего шабанга. Однако способ выполнения расколотых методов может зависеть от типа выражения, переданного оператору await.

В большинстве случаев вы будете использовать await в выражении типа Task. Реализация шаблона await в задании является «умной» в том смысле, что она откладывается до SynchronizationContext, что в основном приводит к следующему:

  1. Если поток, входящий в await, находится в потоке цикла сообщений Dispatcher или WinForms, он гарантирует, что фрагменты асинхронного метода происходят как часть обработки очереди сообщений.
  2. Если поток, вводящий await, находится в потоке пула потоков, то остальные фрагменты асинхронного метода появляются в любом месте пула потоков.

Вот почему вы, вероятно, сталкиваетесь с проблемами - реализация асинхронного метода пытается запустить остальную часть на Dispatcher - даже если она приостановлена.

.... резервное копирование! ....

Я должен задать вопрос, , почему вы пытаетесь синхронно заблокировать асинхронный метод? Это отрицательно скажется на том, почему метод должен вызываться асинхронно. В общем, когда вы начинаете использовать await в методе Dispatcher или UI, вам нужно включить асинхронность всего потока UI. Например, если ваш стек вызовов был примерно таким:

  1. [Top] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing() - WPF или WinForms Код
  6. [Message Loop] - WPF или WinForms Message Loop

Затем, после преобразования кода в асинхронный, вы, как правило, получите

  1. [Top] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing() - WPF или WinForms Код
  6. [Message Loop] - WPF или WinForms Message Loop

Фактически отвечая

Класс AsyncHelpers, приведенный выше, на самом деле работает, потому что он ведет себя как вложенный цикл обработки сообщений, но он устанавливает свою собственную параллельную механику для Dispatcher, а не пытается выполнить ее на самом Dispatcher. Это один из способов решения вашей проблемы.

Другой обходной путь - выполнение асинхронного метода в потоке потоков и ожидание его завершения. Сделать это легко - вы можете сделать это с помощью следующего фрагмента:

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

Конечным API будет Task.Run (...), но для CTP вам понадобятся суффиксы Ex ( объяснение здесь ).

18 голосов
/ 26 октября 2016

Самый простой способ запустить задачу синхронно и без блокировки потока пользовательского интерфейса - это использовать RunSynchronously (), например:

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

В моем случае у меня есть событие, которое срабатывает, когда что-то происходит. Я не знаю, сколько раз это произойдет. Итак, я использую приведенный выше код в своем событии, поэтому всякий раз, когда он запускается, он создает задачу. Задачи выполняются синхронно, и это прекрасно работает для меня. Я был просто удивлен, что мне понадобилось так много времени, чтобы понять это, учитывая, насколько это просто. Обычно рекомендации намного сложнее и подвержены ошибкам. Это было просто и чисто.

16 голосов
/ 05 октября 2013

Я сталкивался с этим несколько раз, в основном в модульном тестировании или в разработке службы Windows. В настоящее время я всегда использую эту функцию:

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

Это просто, легко, и у меня не было проблем.

13 голосов
/ 02 декабря 2014

Я нашел этот код в компоненте Microsoft.AspNet.Identity.Core, и он работает.

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