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

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

这有可能吗?

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

现在我正在研究从sync方法调用这些async方法。

1102442 次浏览

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

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

如果你真的不能让你的方法async并且你不想锁定同步方法,那么你将不得不使用回调方法,将其作为参数传递给任务上的ContinueWith()方法。

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 herestring result = myTask.Result; // wait for the task to complete to continue// use result
}

您将“wait”关键字阅读为“启动此长时间运行的任务,然后将控制权返回给调用方法”。一旦长时间运行的任务完成,它就会执行其后的代码。wait之后的代码类似于过去的CallBack方法。最大的区别是逻辑流程没有中断,这使得编写和阅读变得更容易。

异步编程确实通过代码库“增长”。它一直是跟僵尸病毒相比。最好的解决方案是允许它增长,但有时这是不可能的。

我在我的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方法使用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请求上下文。在这种情况下,您不妨将ConfigureAwait(false)添加到其await语句中,并使用解决方案A。

更新,2019-05-01:当前的“最差实践”在MSDN文章在这里中。

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

因此,如果您想直接调用此传播逻辑,您可以使用task.GetAwaiter().GetResult()

Microsoft构建了一个AsyncHelper(内部)类来将Async作为Sync运行。源代码如下所示:

internal static class AsyncHelper{private static readonly TaskFactory _myTaskFactory = newTaskFactory(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. Ident基类只有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));}

对于那些关心代码许可条款的人,这里有一个指向非常相似的代码的链接(只是在线程上添加了对文化的支持),它有注释表明它是由Microsoft许可的。https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs

这难道不与调用Task. Run(async()=>wait AsyncFunc())相同吗?AFAIK,Microsoft现在不鼓励调用TaskFactory. StartNew,因为它们都是等效的,一个比另一个更具可读性。

绝对不行。

简单的答案是

.Unwrap().GetAwaiter().GetResult() != .Result

首先关闭

Task. Result是否与. GetAwaiter. GetResult()相同?

其次.展开()导致任务的设置不会阻止包装的任务。

这应该会让每个人都问

这和调用Task. Run(async()=>wait AsyncFunc())不一样吗?GetAwaiter()。GetResult()

这将是一个那要看情况

关于任务. Start()、任务. Run()和任务. Factory. StartNew()的用法

摘录:

Task. Run使用TaskCreationOptions. DenyChildAttach,这意味着孩子的任务不能附加到父母,它使用TaskScheduler. Default,这意味着将始终使用在线程池上运行任务运行任务。

Task. Factory. StartNew使用TaskScheduler.当前,这意味着当前线程的调度程序,可能是默认但不总是

补充阅读:

指定同步上下文

ASP.NET核心同步上下文

为了额外的安全,像这样调用它不是更好吗AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false)); 这样我们告诉“内部”方法“请不要尝试同步到上层上下文并解除锁定”

非常好的观点,正如大多数对象架构问题一样这取决于

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

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

这些windows异步方法有一个非常漂亮的小方法,称为As任务()。您可以使用它让方法将自身作为任务返回,以便您可以手动调用它的等待()。

例如,在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 synchronouslyDeleteSynchronous("pathGoesHere");
// Do other work here// .....}

希望这有帮助!

然而,有一个很好的解决方案适用于(几乎:请参阅注释)所有情况:即席消息泵(同步上下文)。

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

即席消息泵助手的代码:

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 contextvar syncCtx = new SingleThreadSynchronizationContext(true);SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the functionsyncCtx.OperationStarted();asyncMethod();syncCtx.OperationCompleted();
// Pump continuations and propagate any exceptionssyncCtx.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 contextvar syncCtx = new SingleThreadSynchronizationContext(false);SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completesvar t = asyncMethod();if (t == null) throw new InvalidOperationException("No task provided.");t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptionssyncCtx.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 contextvar syncCtx = new SingleThreadSynchronizationContext(false);SynchronizationContext.SetSynchronizationContext(syncCtx);
// Invoke the function and alert the context to when it completesvar t = asyncMethod();if (t == null) throw new InvalidOperationException("No task provided.");t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
// Pump continuations and propagate any exceptionssyncCtx.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(...));

有关异步泵的更详细的描述,请参阅这里

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

首先阅读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;}}
var result = Task.Run(async () => await configManager.GetConfigurationAsync()).ConfigureAwait(false);
OpenIdConnectConfiguration config = result.GetAwaiter().GetResult();

或者使用这个:

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

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

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

在内部它调用task.GetAwaiter().GetResult(),这很简单,但是它重载了任何async方法,返回TaskTask<T>Task<HttpResponseMessage>…语法糖,宝贝…爸爸喜欢甜食。

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

如果你想运行它同步

MethodAsync().RunSynchronously()

经过几个小时的尝试不同的方法,或多或少的成功,这就是我的结局。它在获得结果时不会以死锁告终,它还会获取并抛出原始异常,而不是包装的异常。

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

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

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(),因为您应该为不返回值的异步方法返回任务不无效(是的,有一些罕见的例外)。

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

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

斯蒂芬·克利里的回答;

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

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

这就是方法;

线程池黑客与阻止黑客类似的方法是将异步工作卸载到线程池,然后在结果任务。使用此黑客的代码看起来像代码如图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元素或ASP.NETHttpContext. Money。

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

使用它如下:

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

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