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

本文关键字:同步方法 调用 异步方法 | 更新日期: 2023-09-27 18:25:07

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

这可能吗?

下面是从异步方法调用这些方法的一个示例:
演练:使用 Async 和 Await 访问 Web(C# 和 Visual Basic(

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

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

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

我在我的Nito.AsyncEx库中编写了一些类型来处理部分异步的代码库。但是,没有适用于所有情况的解决方案。

解决方案 A

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

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

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

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

解决方案 B

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

var result = AsyncContext.RunTask(MyAsyncMethod).Result;
<小时 />

*更新 4/14/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方法使用不ConfigureAwaitawait
  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 请求上下文。在这种情况下,您也可以将ConfigureAwait(false)添加到其await语句中,并使用解决方案 A。

更新:2015 MSDN 文章"异步编程 - 棕地异步开发"由 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;
    }
}

>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 基类只有异步方法,为了将它们调用为 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

这难道不和只调用 Task.Run(async ((=> await AsyncFunc((( 一样吗?结果?AFAIK,Microsoft现在不鼓励调用TaskFactory.StartNew,因为它们都是等效的,并且一个比另一个更具可读性。

绝对不行。

简单的答案是

.Unwrap().GetAwaiter().GetResult() != .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 核心同步上下文

为了额外的安全性,这样称呼它不是更好吗AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false)); 通过这种方式,我们告诉"内部"方法"请不要尝试同步到上层上下文并取消锁定">

真的很棒,正如大多数对象架构问题一样,这取决于

作为一种扩展方法,你是想绝对每次调用都强制这样做,还是让使用该函数的程序员在他们自己的异步调用上配置它? 我可以看到调用三个场景的用例;它很可能不是您在 WPF 中想要的东西,在大多数情况下当然是有意义的,但考虑到 ASP.Net 核心中没有上下文,如果你能保证它是 ASP.Net 核心的内部,那么这并不重要。

async Main 现在是 C# 7.2 的一部分,可以在项目高级构建设置中启用。

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

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

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

您会在很多Microsoft文档中看到它,例如:https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions

我不是 100% 确定,但我相信本博客中描述的技术应该在许多情况下有效:

因此,如果要

直接调用此传播逻辑,可以使用task.GetAwaiter().GetResult()

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 方法。最大的区别是逻辑流不会中断,这使得写入和读取变得更加容易。

然而,有一个很好的解决方案可以在每种情况下工作(几乎:见评论(:一个临时消息泵(同步上下文(。

调用线程

将按预期被阻止,同时仍确保从异步函数调用的所有延续不会死锁,因为它们将被封送到调用线程上运行的临时 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(...));

此处提供了异步泵的更详细说明。

对于任何关注这个问题的人...

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

在内部,它调用task.GetAwaiter().GetResult()这非常简单,但是它被重载以处理任何返回TaskTask<T>Task<HttpResponseMessage>async方法......句法糖,宝贝...爸爸爱吃甜食。

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

var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);
OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();

或者使用这个:

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

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

正如很多人在这里建议的那样,您可以在同步方法中对结果任务调用 Wait() 或 Result,但最终会在该方法中进行阻塞调用,这有点违背了异步的目的。

如果您确实无法使方法async并且不想锁定同步方法,则必须使用回调方法,将其作为参数传递给任务上的ContinueWith()方法。

这是最简单的解决方案。我在互联网上的某个地方看到它,我不记得在哪里,但我一直在成功使用它。它不会使调用线程死锁。

    void SynchronousFunction()
    {
        Task.Run(Foo).Wait();
    }
    string SynchronousFunctionReturnsString()
    {
        return Task.Run(Foo).Result;
    }
    string SynchronousFunctionReturnsStringWithParam(int id)
    {
        return Task.Run(() => Foo(id)).Result;
    }

Stephen Cleary的答案;

这种方法不应该导致死锁(假设问题方法异步不会将更新发送到 UI 线程或任何内容像那样(。它确实假设可以在线程池线程,情况并非总是如此。

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

这是方法;

线程池黑客 与阻塞黑客类似的方法是将异步工作卸载到线程池,然后在结果任务。使用此 hack 的代码看起来像代码如图 7 所示。

图 7 线程池黑客的代码

C#

public sealed class WebDataService : IDataService
{
  public string Get(int id)
  {
    return Task.Run(() => GetAsync(id)).GetAwaiter().GetResult();
  }
  public async Task<string> GetAsync(int id)
  {
    using (var client = new WebClient())
      return await client.DownloadStringTaskAsync(
      "https://www.example.com/api/values/" + id);
  }
}

对 Task.Run 的调用在线程池上执行异步方法线。在这里,它将在没有上下文的情况下运行,从而避免僵局。这种方法的问题之一是异步方法不能依赖于在特定上下文中执行。所以,它不能使用 UI 元素或 HttpContext.Current ASP.NET。

受到其他一些答案的启发,我创建了以下简单的帮助程序方法:

public static TResult RunSync<TResult>(Func<Task<TResult>> method)
{
    var task = method();
    return task.GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> method)
{
    var task = method();
    task.GetAwaiter().GetResult();
}

可以按如下方式调用它们(取决于您是否返回值(:

RunSync(() => Foo());
var result = RunSync(() => FooWithResult());

请注意,原始问题public async void Foo()中的签名不正确。它应该是public async Task Foo()的,因为对于不返回值的异步方法,您应该返回 Task not void(是的,有一些罕见的例外(。

好吧,我多年来一直在使用这种方法,它还处理和传播来自底层异步任务的异常。完美无缺。

private string RunSync()
{
    var task = Task.Run(async () => await GenerateCodeService.GenerateCodeAsync());
    if (task.IsFaulted && task.Exception != null)
    {
        throw task.Exception;
    }
    return task.Result;
}

但既然Microsoft创建了这个异步助手:https://github.com/aspnet/AspNetIdentity/blob/main/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

这也是他们的来源:

public static void RunSync(Func<Task> func)
        {
            var cultureUi = CultureInfo.CurrentUICulture;
            var culture = CultureInfo.CurrentCulture;
            _myTaskFactory.StartNew(() =>
            {
                Thread.CurrentThread.CurrentCulture = culture;
                Thread.CurrentThread.CurrentUICulture = cultureUi;
                return func();
            }).Unwrap().GetAwaiter().GetResult();
        }

现在,可以使用源生成器通过同步方法生成器库 (nuget( 创建方法的同步版本。

按如下方式使用它:

[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async void FooAsync()

这将生成Foo您可以同步调用的方法。

每个人似乎都预设了需要等待结果。我经常需要从我不关心结果的同步方法更新数据。我只使用丢弃:

_ = UpdateAsync();

这些窗口异步方法有一个漂亮的小方法,称为AsTask((。您可以使用它让该方法将自身作为任务返回,以便您可以手动调用 Wait((。

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

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

希望这有帮助!

如果你想运行它 同步

MethodAsync().RunSynchronously()