如何在C#中从同步方法调用异步方法?

1353
我有一个名为public async Task Foo()的方法,我想从一个同步方法中调用它。到目前为止,我在MSDN文档中看到的只有通过async方法调用async方法的方法,但我的整个程序并没有构建在async方法上。
这种情况是否可能?
这里有一个从异步方法中调用这些方法的示例:
通过使用Async和Await访问Web的演练(C#和Visual Basic) 现在我正在研究如何从同步方法中调用这些async方法。

4
我也遇到了这个问题。覆盖RoleProvider时,您无法更改GetRolesForUser方法的方法签名,因此无法使该方法异步,并且无法使用await异步调用api。我的临时解决方案是将同步方法添加到我的通用HttpClient类中,但我想知道是否可能(以及可能的影响)。 - Timothy Lee Russell
3
由于您的async void Foo()方法没有返回Task,这意味着调用者无法知道它何时完成,必须改为返回Task - Dai
1
在 UI 线程上执行此操作的相关问题/答案链接:https://dev59.com/KlQJ5IYBdhLWcg3wl3Fa。 - noseratio - open to work
2
我已经使用了这种方法,似乎可以完成工作:MyMethodAsync.GetAwaiter().GetResult(); 在此之前,您可能需要查看以下文章,该文章最终归结为死锁和线程池饥饿问题: https://medium.com/rubrikkgroup/understanding-async-avoiding-deadlocks-e41f8f2c6f5d - J.Tribbiani
@Timothy Lee Russell,我认为GetRolesForUser()不应该做太多的事情,特别是不应该调用耗时的异步方法。 - The incredible Jan
@Dai,我编辑了这个问题并纠正了这个(很可能是无意的)错误。 - undefined
18个回答

1070
异步编程在代码库中是会"增长"的。它曾被比作僵尸病毒。最好的解决方案是让其自由发展,但有时这是不可能的。
我在我的Nito.AsyncEx库中编写了几种类型,用于处理部分异步代码库。然而,并没有适用于每种情况的解决方案。 解决方案A 如果你有一个简单的异步方法,不需要回到其上下文中进行同步,那么你可以使用Task.WaitAndUnwrapException
var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();

你不想使用 Task.Wait 或者 Task.Result,因为它们会将异常包装在 AggregateException 中。

这个解决方案仅适用于 MyAsyncMethod 不需要回到其上下文的情况。换句话说,在 MyAsyncMethod 的每个 await 结尾都应该加上 ConfigureAwait(false)。这意味着它不能更新任何 UI 元素或访问 ASP.NET 请求上下文。

解决方案 B

如果 MyAsyncMethod 需要回到其上下文进行同步,那么你可以尝试使用 AsyncContext.RunTask 来提供一个嵌套的上下文:

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

*更新于2014年4月14日:在库的最新版本中,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 不允许在该上下文中运行继续任务,因为同步方法已经在那个上下文中运行。
这也是为什么尽可能在每个 async 方法中使用 ConfigureAwait(false) 是一个好主意的原因之一。

解决方案 C

AsyncContext.RunTask 在每种情况下都不适用。例如,如果 async 方法等待需要完成 UI 事件的内容,即使有嵌套上下文,你仍然会发生死锁。在这种情况下,你可以在线程池上启动 async 方法:

var task = Task.Run(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();

然而,这个解决方案需要一个能在线程池上下文中工作的MyAsyncMethod。因此,它无法更新UI元素或访问ASP.NET请求上下文。在这种情况下,您可以在其await语句中添加ConfigureAwait(false),并使用解决方案A。
更新:2015年Stephen Cleary的MSDN文章“异步编程 - Brownfield异步开发”。

22
方案 A 看起来就像我想要的,但是它似乎没有包含 task.WaitAndUnwrapException() 方法在 .NET 4.5 RC 中;它只有 task.Wait()。你有什么方法在新版本中实现这个功能吗?还是这是你编写的自定义扩展方法? - deadlydog
6
WaitAndUnwrapException 是我自己的方法,来自于我的 AsyncEx 库。官方的 .NET 库对于混合使用同步和异步代码提供的帮助不多(通常情况下,你不应该这样做!)。我在等待 .NET 4.5 RTW 和一台新的非 XP 笔记本电脑,然后才会更新 AsyncEx 以在 4.5 上运行(目前我无法为 4.5 进行开发,因为我还需要几个星期继续使用 XP)。 - Stephen Cleary
19
现在AsyncContext有一个接受lambda表达式的Run方法,因此您应该使用var result = AsyncContext.Run(() => MyAsyncMethod()); - Stephen Cleary
4
@Asad: 是的,两年过去了,API 已经发生变化。现在,你可以简单地使用 var result = AsyncContext.Run(MyAsyncMethod);。更多详情请参考这个链接:http://nitoasyncex.codeplex.com/wikipage?title=AsyncContext。 - Stephen Cleary
10
请安装 Nito.AsyncEx 库。 或者使用 .GetAwaiter().GetResult() 替代 .WaitAndUnwrapException() - Stephen Cleary
显示剩余34条评论

430

分享一个最终解决我的问题的方法,希望能够节省某人的时间。

首先阅读几篇Stephen Cleary的文章:

从“不要在异步代码上阻塞”中的“两个最佳实践”中,第一个对我没有用,而第二个不适用(基本上如果我可以使用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;
    }
}

9
两年过去了,我很想知道这个解决方案现在的情况。有什么新消息吗?这种方法是否有一些细微之处是新手难以理解的? - Dan Esparza
53
这不会死锁,但这只是因为它被强制在一个新的线程中运行,不在原线程的同步上下文中。然而,在某些环境中,这是非常不明智的,尤其是在Web应用程序中。这可能会将Web服务器可用线程数减半(一个线程用于请求,一个线程用于此)。你做得越多,情况就越糟。你可能最终会导致整个Web服务器死锁。 - Chris Pratt
63
@ChrisPratt - 你说得可能对,因为在异步代码中使用Task.Run()不是最佳实践。但是,再次强调,对于原始问题的答案是什么呢?永远不要同步调用异步方法吗?我们希望这样做,但在现实世界中,有时我们不得不这么做。 - Tohid
57
.NET 5.0 已经发布了,却仍然没有一种完全可靠的方法可以将异步方法同步调用,这有点疯狂。 - Mass Dot Net
4
在第一种情况下,任务在后台线程上执行,并通过Result属性由原始上下文获取结果。在第二种情况下,任务在同一上下文中执行,如果该上下文为UI线程,将导致死锁。 - AsPas
显示剩余9条评论

309
Microsoft建立了一个AsyncHelper(内部)类来将异步操作作为同步操作运行。源代码如下:
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的基类只有异步方法,为了以同步方式调用它们,需要使用扩展方法的类(示例用法):
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/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs “这难道不就等同于调用Task.Run(async () => await AsyncFunc()).Result吗?据我所知,微软现在不鼓励使用TaskFactory.StartNew,因为它们两者是等价的,而且一个比另一个更易读。”
绝对不是。
简单的答案是:
.Unwrap().GetAwaiter().GetResult() != .Result

首先,

Task.Result 与 .GetAwaiter.GetResult() 相同吗?

其次,.Unwrap() 会导致任务设置不阻塞封装的任务。

这应该引导任何人提出以下问题:

这难道不就等同于调用 Task.Run(async ()=> await AsyncFunc()).GetAwaiter().GetResult() 吗?

然后,就会变成 这取决于情况

关于 Task.Start()、Task.Run() 和 Task.Factory.StartNew() 的使用

摘录:

Task.Run使用TaskCreationOptions.DenyChildAttach,这意味着子任务无法附加到父任务,并且它使用TaskScheduler.Default,这意味着总是使用线程池来运行任务
Task.Factory.StartNew使用TaskScheduler.Current,这意味着当前线程的调度器,可能是TaskScheduler.Default,但不总是
额外阅读: 指定同步上下文 ASP.NET Core的同步上下文 为了更安全,调用应该像这样AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));这样我们告诉"内部"方法"请不要尝试与上层上下文同步并导致死锁"。

通过alex-from-jitbit提出的观点非常好,就大多数对象架构问题而言都是因情况而异

作为一个扩展方法,你是否想要强制对每次调用都进行配置,还是让程序员在使用函数时自行配置异步调用?我可以看到有三种调用场景的用例;在 WPF 中可能不是你想要的,但在大多数情况下是有意义的,但考虑到 ASP.Net Core 中没有 Context,如果你能保证它在 ASP.Net Core 中是 say internal 的,那就无所谓了。


4
我的异步方法等待其他异步方法完成。我在任何await调用中都没有使用ConfigureAwait(false)修饰符。我尝试使用AsyncHelper.RunSync从Global.asax的Application_Start()函数调用异步函数,似乎可以工作。这是否意味着AsyncHelper.RunSync可靠地不容易出现在本帖子中其他地方所提到的“返回调用者上下文”的死锁问题? - Bob.at.Indigo.Health
1
谢谢。两个后续问题:1)您能举一个async方法想要避免的会导致死锁的示例吗?2)在这种情况下,死锁是否经常依赖于时间?如果在实践中可行,那么我是否仍然可能在我的代码中潜伏着一个依赖于时间的死锁? - Bob.at.Indigo.Health
1
@Bob.at... Erik 提供的代码在 Asp.net mvc5 和 EF6 下运行得非常完美,但是当我尝试使用其他解决方案(如 ConfigureAwait(false).GetAwaiter().GetResult() 或 .result)时,我的 Web 应用程序会完全挂起。 - LeonardoX
1
为了更加安全,这样调用会更好 AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false)); 这样我们告诉“内部”方法“请不要尝试同步到上下文并发生死锁”。 - Alex from Jitbit
1
@AlexfromJitbit 很棒的观点,问题已更新! - Erik Philips
显示剩余11条评论

262

22
我不知道为什么有人会反对这个。这对我很有用。如果没有这个修复程序,我就必须在所有地方传播异步。 - Prisoner ZERO
21
为什么这比MainAsync().Wait()更好? - crush
14
我同意。你只需要使用MainAsync().Wait(),而不是所有这些代码。 - Hajjat
14
@crush,我在描述如何避免一些死锁情况。在某些情况下,从UI或ASP.NET线程调用.Wait()可能会导致死锁。更多异步死锁信息请参考:http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - David
8
@ClintB:在ASP.NET Core中绝对不应该这样做。Web应用程序特别容易出现线程饥饿的情况,每次这样做时,你都会从池中拉取一个本来可以用于服务请求的线程。对于桌面/移动应用程序来说问题较小,因为它们传统上是单用户的。 - Chris Pratt
显示剩余14条评论

76

我不确定,但我认为在这篇博客中所描述的技术,在许多情况下都能起作用:

如果你想直接调用这种传播逻辑,你可以使用 task.GetAwaiter().GetResult()


12
Stephen Cleary在他的回答中使用了A解决方案,该方法使用了这种技术。请参见WaitAndUnwrapException源代码。 - orad
如果你调用的函数是 void 或 task 类型,且不需要获取任何结果,那么你是否需要使用 GetResult() 函数呢? - Emil
1
是的,否则它不会阻塞直到任务完成。或者,您可以调用.Wait()而不是调用GetAwaiter().GetResult()。 - NStuke
2
那就是“许多情况”的部分。它取决于整体线程模型以及其他线程的操作,以确定是否存在死锁风险。 - NStuke
对我来说,GetAwaiter().GetResult()总是会导致死锁。 - Aidan
显示剩余2条评论

68
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 后面的代码类似于以前的回调函数。主要的区别是逻辑流不会被中断,这使得编写和阅读更加容易。


24
"Wait"会包装异常并有可能导致死锁。 - Stephen Cleary
注意:在调用Wait()之后,您可以通过调用myTask.Result()来获取类型为T的结果。 - Eric J.
好的观点。我已经将Wait()更改为Result,因为这也会阻塞直到任务完成。 - Despertar
3
这个答案至今是否仍然有效?我刚在一个MVC Razor项目中尝试了一下,但是应用程序在访问.Result时卡住了。 - iCollect.it Ltd
8
这是同步上下文死锁。你的异步代码会回到同步上下文,但此时同步上下文被“Result”调用阻塞,所以异步代码永远无法到达同步上下文。而“Result”也永远无法结束,因为它正在等待某个正在等待“Result”结束的人,基本上就是这样:D - Luaan
显示剩余2条评论

32

然而,有一个解决方案在几乎所有情况下都可以使用:即时消息泵(同步上下文)。

调用线程将像预期的那样被阻塞,同时确保从异步函数调用的所有继续调用不会死锁,因为它们将被编组到运行在调用线程上的临时同步上下文(消息泵)中。

即时消息泵助手的代码:

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

这里有更详细的异步泵的描述。


异常上下文和AsyncPump https://dev59.com/in7aa4cB1Zd3GeqPvt3R - PreguntonCojoneroCabrón
3
在 Asp.net 场景下,这种方法行不通,因为 HttpContext.Current 可能会被随机清除而丢失。 - Josh Mouch
1
@JoshMouch 除非你使用的是非常老的版本的asp.net,否则你永远不应该使用HttpContext.Current。 - Erik Philips

24

对于仍然关注此问题的任何人...

如果您查看 Microsoft.VisualStudio.Services.WebApi,则会发现一个名为 TaskExtensions 的类。在该类中,您将看到静态扩展方法 Task.SyncResult(),其完全会阻塞线程直到任务返回。

它内部调用 task.GetAwaiter().GetResult(),这非常简单,但是它被重载以适用于返回 TaskTask<T> 或者 Task<HttpResponseMessage> 的任何 async 方法... 糖衣炮弹,宝贝... 爸爸有个甜蜜的牙齿。

看起来 ...GetAwaiter().GetResult() 是 MS 官方的在阻塞上下文中执行异步代码的方式。对于我的使用情况似乎非常有效。


6
你让我一下子就感兴趣了,“完全只是几个街区”的那个词。 - Dawood ibn Kareem
3
对我来说,task.GetAwaiter().GetResult() 总是导致死锁。 - Aidan

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

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

或者使用这个:

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

15

您可以从同步代码中调用任何异步方法,但是在需要使用await时,它们也必须标记为async

像许多人在这里建议的那样,您可以在同步方法中调用结果任务上的Wait()Result,但这会使该方法变成阻塞调用,这有点违背了异步的初衷。

如果您真的无法将方法设为async,并且不想锁定同步方法,那么您就必须通过将其作为参数传递给任务上的ContinueWith()方法来使用回调函数。


11
那样做就不算同步调用该方法了,不是吗? - Jeff Mercado
6
我理解这个问题是关于是否可以从一个非异步方法调用一个异步方法。这并不意味着必须以阻塞的方式调用异步方法。 - base2
2
抱歉,你的“它们也必须被标记为async”分散了我的注意力,让我无法理解你真正想表达的意思。 - Jeff Mercado
2
如果我不太关心异步性,这样调用可以吗?(Stephen Cleary一直在唠叨包装异常中的死锁可能性怎么办?)我有一些测试方法(必须同步执行),测试异步方法。我必须等待结果才能继续,以便测试异步方法的结果。 - awe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接