Как вызвать асинхронный метод из синхронного метода в C #? - PullRequest
665 голосов
/ 18 февраля 2012

У меня есть метод public async void Foo(), который я хочу вызвать из синхронного метода. До сих пор все, что я видел из документации MSDN, это вызов асинхронных методов через асинхронные методы, но вся моя программа не построена с асинхронными методами.

Возможно ли это вообще?

Вот один пример вызова этих методов из асинхронного метода: http://msdn.microsoft.com/en-us/library/hh300224(v=vs.110).aspx

Теперь я собираюсь вызвать эти асинхронные методы из методов синхронизации.

Ответы [ 14 ]

574 голосов
/ 18 февраля 2012

Асинхронное программирование «растет» через базу кода. Это было по сравнению с зомби-вирусом . Лучшее решение - позволить ему расти, но иногда это невозможно.

Я написал несколько типов в моей Nito.AsyncEx библиотеке для работы с частично асинхронной базой кода. Однако нет решения, которое бы работало в любой ситуации.

Раствор А

Если у вас есть простой асинхронный метод, который не нуждается в синхронизации обратно в его контекст, тогда вы можете использовать Task.WaitAndUnwrapException:

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

Вы не хотите использовать Task.Wait или Task.Result, потому что они включают исключения в AggregateException.

Это решение подходит, только если MyAsyncMethod не синхронизируется обратно с его контекстом. Другими словами, каждый await в MyAsyncMethod должен заканчиваться ConfigureAwait(false). Это означает, что он не может обновлять какие-либо элементы пользовательского интерфейса или обращаться к контексту запроса ASP.NET.

Раствор B

Если MyAsyncMethod необходимо синхронизироваться обратно с его контекстом, то вы можете использовать AsyncContext.RunTask для предоставления вложенного контекста:

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

* Обновление от 14.04.2014: В более поздних версиях библиотеки API выглядит следующим образом:

var result = AsyncContext.Run(MyAsyncMethod);

(в этом примере можно использовать Task.Result, потому что RunTask будет распространять Task исключений).

Причина, по которой вам может потребоваться AsyncContext.RunTask вместо Task.WaitAndUnwrapException, заключается в том, что в WinForms / WPF / SL / ASP.NET возможна довольно тонкая тупиковая ситуация:

  1. Синхронный метод вызывает асинхронный метод, получая Task.
  2. Синхронный метод блокирует ожидание на Task.
  3. Метод async использует await без ConfigureAwait.
  4. Task не может завершиться в этой ситуации, потому что он завершается только после завершения метода async; метод async не может завершиться, поскольку он пытается запланировать свое продолжение на SynchronizationContext, а WinForms / WPF / SL / ASP.NET не разрешит запуск продолжения, поскольку синхронный метод уже запущен в этом контексте.

Это одна из причин, по которой рекомендуется использовать ConfigureAwait(false) в каждом async методе как можно больше.

Раствор C

AsyncContext.RunTask не будет работать в каждом сценарии. Например, если метод async ожидает чего-то, что требует события пользовательского интерфейса для завершения, то вы будете в тупике даже с вложенным контекстом. В этом случае вы можете запустить метод async в пуле потоков:

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

Однако для этого решения требуется MyAsyncMethod, который будет работать в контексте пула потоков. Поэтому он не может обновлять элементы пользовательского интерфейса или обращаться к контексту запроса ASP.NET. И в этом случае вы также можете добавить ConfigureAwait(false) к его await операторам и использовать решение A.

Обновление, 2019-05-01: Текущие "наименьшие наихудшие практики" содержатся в статье MSDN здесь .

226 голосов
/ 29 декабря 2015

Добавление решения, которое окончательно решило мою проблему, надеюсь, сэкономит кому-то время.

Сначала прочитайте пару статей Стивен Клири :

Из "двух лучших практик" в "Не блокировать в асинхронном коде" первый не работал для меня, а второй не применим (в основном, если я могу использовать await, я !).

Итак, вот мой обходной путь: заверните вызов в Task.Run<>(async () => await FunctionAsync()); и, надеюсь, больше не тупик .

Вот мой код:

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

Microsoft создала класс AsyncHelper (внутренний) для запуска Async как Sync. Источник выглядит так:

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

Базовые классы Microsoft.AspNet.Identity имеют только методы Async, и для вызова их как Sync существуют классы с методами расширения, которые выглядят следующим образом (пример использования):

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

Для тех, кто обеспокоен условиями лицензирования кода, приведена ссылка на очень похожий код (просто добавлена ​​поддержка культуры в потоке), в которой есть комментарии, указывающие, что это лицензия MIT от Microsoft. https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

117 голосов
/ 24 февраля 2015

async Main теперь является частью C # 7.2 и может быть включен в расширенных настройках проекта.

Для C # <7.2 правильный путь: </p>

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


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

Вы увидите, что это используется во многих документах Microsoft, например: https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions

48 голосов
/ 18 февраля 2012
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

}

Вы прочитали ключевое слово await как «запустите эту долгосрочную задачу, а затем верните управление вызывающему методу».Как только долгосрочное задание выполнено, оно выполняет код после него.Код после await похож на то, что раньше было методами CallBack.Большая разница в том, что логический поток не прерывается, что делает его намного легче писать и читать.

31 голосов
/ 27 марта 2014

Я не уверен на 100%, но я верю, что методика, описанная в этом блоге , должна работать во многих случаях:

Таким образом, вы можете использовать task.GetAwaiter().GetResult(), если хотите напрямую вызывать эту логику распространения.

23 голосов
/ 25 августа 2015

Самый принятый ответ не совсем правильный.Существует решение, которое работает в любой ситуации: специальный насос сообщений (SynchronizationContext).

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

Код вспомогательного помощника насоса сообщений:

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

Использование:

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

Более подробное описание асинхронного насоса доступно здесь .

6 голосов
/ 22 декабря 2018

Для тех, кто больше обращает внимание на этот вопрос ...

Если вы заглянете в Microsoft.VisualStudio.Services.WebApi, есть класс с именем TaskExtensions.Внутри этого класса вы увидите статический метод расширения Task.SyncResult(), который как бы просто блокирует поток до тех пор, пока задача не вернется.

Внутренне он вызывает task.GetAwaiter().GetResult(), что довольно просто, однако он перегружен для работы с ним.любой async метод, который возвращает Task, Task<T> или Task<HttpResponseMessage> ... синтаксический сахар, детка ... у папы сладкоежка.

Похоже, ...GetAwaiter().GetResult() является MS-официальный способ выполнения асинхронного кода в контексте блокировки.Кажется, работает очень хорошо для моего случая использования.

3 голосов
/ 18 февраля 2012

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

Как много людейПредлагаем здесь, вы можете вызвать Wait () или Result для результирующей задачи в вашем синхронном методе, но затем вы получите блокирующий вызов в этом методе, который побеждает цель асинхронности.

Если вы действительно не можете сделать свой метод async и вы не хотите блокировать синхронный метод, тогда вам придется использовать метод обратного вызова, передав его в качестве параметраМетод ContinueWith для задачи.

2 голосов
/ 09 марта 2019

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

Я нашел следующий фрагмент кода из Райан

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

тогда вы можете назвать это так

var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...