关于async await:如何从C#中的同步方法调用异步方法?

How to call asynchronous method from synchronous method in C#?

我有一个public async void Foo()方法,我想从同步方法调用它。到目前为止,我从msdn文档中看到的只是通过异步方法调用异步方法,但我的整个程序并不是用异步方法构建的。

这是可能的吗?

以下是从异步方法调用这些方法的一个示例:http://msdn.microsoft.com/en-us/library/hh300224(v=v s.110).aspx

现在我正在研究从同步方法调用这些异步方法。


异步编程通过代码库"增长"。它被比作一种僵尸病毒。最好的解决方案是允许它增长,但有时这是不可能的。

我在nito.asyncEx库中编写了一些类型,用于处理部分异步的代码基。但在任何情况下都没有解决方案。

解决方案A

如果您有一个简单的异步方法不需要同步回其上下文,那么您可以使用Task.WaitAndUnwrapException

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

您不想使用Task.WaitTask.Result,因为它们将异常包装在AggregateException中。

只有当MyAsyncMethod不同步回其上下文时,此解决方案才适用。换句话说,MyAsyncMethod中的每个await都应该以ConfigureAwait(false)结尾。这意味着它不能更新任何UI元素或访问ASP.NET请求上下文。

解决方案B

如果MyAsyncMethod确实需要同步回其上下文,那么您可以使用AsyncContext.RunTask提供嵌套上下文:

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

*2014年4月14日更新:在库的最新版本中,API如下:

1
var result = AsyncContext.Run(MyAsyncMethod);

(本例中可以使用Task.Result,因为RunTask将传播Task异常)。

您可能需要AsyncContext.RunTask而不是Task.WaitAndUnwrapException的原因是因为在winforms/wpf/sl/asp.net上发生的非常微妙的死锁可能性:

  • 同步方法调用异步方法,获取Task
  • 同步方法在Task上执行阻塞等待。
  • async方法使用没有ConfigureAwaitawait
  • 在这种情况下,Task无法完成,因为它只在async方法完成时才完成;async方法无法完成,因为它正试图将其继续计划到SynchronizationContext中,而winforms/wpf/sl/asp.net不允许继续运行,因为同步方法已经完成。在那种环境下运行。
  • 这就是为什么在每个async方法中尽可能多地使用ConfigureAwait(false)是一个好主意的原因之一。

    C解决方案

    AsyncContext.RunTask不会在每种情况下都起作用。例如,如果async方法等待某个需要UI事件才能完成的操作,那么即使使用嵌套上下文,也会死锁。在这种情况下,可以在线程池上启动async方法:

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

    但是,这个解决方案需要一个在线程池上下文中工作的MyAsyncMethod。因此它无法更新UI元素或访问ASP.NET请求上下文。在这种情况下,您还可以将ConfigureAwait(false)添加到其await语句中,并使用解决方案A。

    更新,2019-05-01:当前的"最差实践"在这里的一篇msdn文章中。


    添加一个解决方案,最终解决了我的问题,希望能节省别人的时间。

    首先阅读斯蒂芬·克利里的几篇文章:

    • 异步等待
    • 不要阻止异步代码

    从"不要阻塞异步代码"中的"两个最佳实践"来看,第一个不适用于我,第二个不适用(基本上如果我可以使用await,我就可以!).

    所以这里是我的解决方法:将调用包装在一个Task.Run<>(async () => await FunctionAsync());中,希望不再出现死锁。

    这是我的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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;
        }
    }


    Microsoft构建了一个AsyncHelper(内部)类,以同步方式运行Async。资料来源如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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基类只有异步方法,为了将它们作为同步调用,有一些类的扩展方法如下(示例用法):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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。https://github.com/aspnet/aspnetidity/blob/master/src/microsoft.aspnet.identity.core/asyncHelper.cs


    AsyncMain现在是C 7.2的一部分,可以在项目高级生成设置中启用。

    对于c<7.2,正确的方法是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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-订阅


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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"关键字读为"启动这个长时间运行的任务,然后将控制权返回到调用方法"。一旦长时间运行的任务完成,它就在它之后执行代码。wait之后的代码类似于以前的回调方法。最大的区别是逻辑流没有中断,这使得写和读更容易。


    我不是百分之百确定,但我相信这个博客中描述的技术在很多情况下都能发挥作用:

    You can thus use task.GetAwaiter().GetResult() if you want to directly invoke this propagation logic.


    最普遍接受的答案并不完全正确。在每种情况下都有一种解决方案:即席消息泵(SynchronizationContext)。

    调用线程将按预期被阻塞,同时仍然确保从异步函数调用的所有延续不会死锁,因为它们将被封送到调用线程上运行的临时同步上下文(消息泵)。

    特设消息泵帮助程序的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    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();
                }
            }
        }
    }

    用途:

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

    这里提供了异步泵的更详细的描述。


    任何关注这个问题的人…

    如果你看Microsoft.VisualStudio.Services.WebApi,有一个类叫做TaskExtensions。在这个类中,您将看到静态扩展方法Task.SyncResult(),它就像完全阻塞线程直到任务返回。

    在内部它称为task.GetAwaiter().GetResult(),这很简单,但是对于任何返回TaskTaskTaskasync方法来说,它都是超负荷的。语法糖,宝贝…爸爸有颗甜牙。

    看起来...GetAwaiter().GetResult()是MS在阻塞上下文中执行异步代码的官方方式。对于我的用例来说似乎工作得很好。


    您可以从同步代码调用任何异步方法,也就是说,在需要对它们执行await之前,在这种情况下,它们也必须标记为async

    正如很多人在这里建议的那样,您可以在同步方法中调用wait()或对结果任务执行result,但最终会在该方法中调用一个阻塞调用,这有点破坏了异步的目的。

    如果你真的不能使你的方法async并且你不想锁定同步方法,那么你必须使用回调方法,将它作为参数传递给continuewith方法on task。


    我知道我迟到了。但如果像我这样的人想用一种整洁、简单的方式解决这个问题,而不依赖于另一个库。

    我找到了Ryan的以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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();
    }

    然后你可以这样称呼它

    1
    var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());


    1
    2
    3
    var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);

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

    或使用此:

    1
    var result=result.GetAwaiter().GetResult().AccessToken


    如果要运行它,请同步

    1
    MethodAsync().RunSynchronously()


    这些Windows异步方法有一个漂亮的小方法,叫做astask()。您可以使用此方法将该方法本身作为任务返回,以便可以手动对其调用wait()。

    例如,在Windows Phone 8 Silverlight应用程序上,可以执行以下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private void DeleteSynchronous(string path)
    {
        StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
        Task t = localFolder.DeleteAsync(StorageDeleteOption.PermanentDelete).AsTask();
        t.Wait();
    }

    private void FunctionThatNeedsToBeSynchronous()
    {
        // Do some work here
        // ....

        // Delete something in storage synchronously
        DeleteSynchronous("pathGoesHere");

        // Do other work here
        // .....
    }

    希望这有帮助!


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
       //Example from non UI thread -    
       private void SaveAssetAsDraft()
        {
            SaveAssetDataAsDraft();
        }
        private async Task<bool> SaveAssetDataAsDraft()
        {
           var id = await _assetServiceManager.SavePendingAssetAsDraft();
           return true;  
        }
       //UI Thread -
       var result = Task.Run(() => SaveAssetDataAsDraft().Result).Result;