我有一个要从同步方法调用的 public async void Foo()
方法。到目前为止,我从 MSDN 文档中看到的只是通过异步方法调用异步方法,但我的整个程序不是用异步方法构建的。
这甚至可能吗?
下面是从异步方法调用这些方法的一个示例:
Walkthrough: Accessing the Web by Using Async and Await (C# and Visual Basic)
现在我正在研究从同步方法中调用这些异步方法。
async void Foo()
方法不返回 Task
,这意味着调用者无法知道它何时完成,它必须返回 Task
。
异步编程确实通过代码库“增长”。已经是 compared to a zombie virus。最好的解决方案是让它成长,但有时这是不可能的。
我在我的 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 上发生了相当微妙的死锁可能性:
同步方法调用异步方法,获得一个任务。同步方法对 Task 进行阻塞等待。 async 方法使用 await 而不使用 ConfigureAwait。 Task 在这种情况下无法完成,因为它只有在 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 年 5 月 1 日更新:当前的“最坏做法”在 MSDN article here 中。
添加一个最终解决我的问题的解决方案,希望能节省一些人的时间。
首先阅读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;
}
}
Task.Run()
不是异步代码中的最佳实践。但是,再一次,原始问题的答案是什么?永远不要同步调用异步方法?我们希望,但在现实世界中,有时我们必须这样做。
Parallel.ForEach
滥用不会对“现实世界”产生影响,最终导致服务器瘫痪。此代码适用于控制台应用程序,但正如 @ChrisPratt 所说,不应在 Web 应用程序中使用。它可能“现在”工作,但不可扩展。
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 许可。 https://github.com/aspnet/AspNetIdentity/blob/master/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs
这和调用 Task.Run(async ()=> await AsyncFunc()).Result 不一样吗? AFAIK,微软现在不鼓励调用 TaskFactory.StartNew,因为它们都是等价的,而且一个比另一个更具可读性。
绝对不。
简单的答案是
.Unwrap().GetAwaiter().GetResult() != .Result
首先关闭
Is Task.Result the same as .GetAwaiter.GetResult()?
其次 .Unwrap() 导致任务的设置不阻止包装的任务。
这应该导致任何人问
这与调用 Task.Run(async ()=> await AsyncFunc()).GetAwaiter().GetResult() 不一样吗
那么这将是一个It Depends。
Regarding usage of Task.Start() , Task.Run() and Task.Factory.StartNew()
摘抄:
Task.Run 使用 TaskCreationOptions.DenyChildAttach ,这意味着子任务不能附加到父任务,它使用 TaskScheduler.Default ,这意味着在线程池上运行任务的任务将始终用于运行任务。 Task.Factory.StartNew 使用 TaskScheduler.Current 表示当前线程的调度程序,它可能是 TaskScheduler.Default 但并非总是如此。
补充阅读:
Specifying a synchronization context
ASP.NET Core SynchronizationContext
为了额外的安全,最好这样调用它 AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));这样我们就告诉“内部”方法“请不要尝试同步到上层上下文并解除锁定”
真的很重要,因为大多数对象架构问题都取决于它。
作为一种扩展方法,您是希望 absolutely 每次调用都强制这样做,还是让使用该函数的程序员在他们自己的异步调用中配置它?我可以看到调用三个场景的用例;它很可能不是您在 WPF 中想要的东西,在大多数情况下当然是有道理的,但是考虑到没有 Context in ASP.Net Core,如果您可以保证它是 ASP.Net Core 的内部,那么它就没有关系了。
ConfigureAwait(false)
装饰我的任何 await
调用。我尝试使用 AsyncHelper.RunSync
从 Global.asax 中的 Application_Start()
函数调用异步函数,它似乎工作。这是否意味着 AsyncHelper.RunSync
可靠地不容易出现我在这篇帖子的其他地方读到的“封送回调用者的上下文”死锁问题?
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://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions
MainAsync().Wait()
好?
我不能 100% 确定,但我相信 this blog 中描述的技术应该适用于许多情况:
因此,如果您想直接调用此传播逻辑,则可以使用 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 之后的代码类似于以前的 CallBack 方法。最大的区别是逻辑流程没有中断,这使得编写和阅读变得更加容易。
Wait
包装异常并有可能出现死锁。
await
的情况下调用异步方法,它将同步执行。至少这对我有用(无需调用 myTask.Wait
)。实际上,当我尝试调用 myTask.RunSynchronously()
时出现异常,因为它已经被执行了!
.Result
时挂起。
Result
调用阻止,因此它永远不会到达那里。而且 Result
永远不会结束,因为它正在等待等待 Result
结束的人,基本上:D
然而,有一个很好的解决方案(几乎:见评论)在每一种情况下都有效:即席消息泵(SynchronizationContext)。
调用线程将按预期被阻塞,同时仍确保从异步函数调用的所有延续不会死锁,因为它们将被编组到调用线程上运行的临时 SynchronizationContext(消息泵)。
ad-hoc 消息泵助手的代码:
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(...));
here 提供了有关异步泵的更详细说明。
对于任何关注这个问题的人......
如果您查看 Microsoft.VisualStudio.Services.WebApi
,则有一个名为 TaskExtensions
的类。在该类中,您将看到静态扩展方法 Task.SyncResult()
,它完全阻塞线程直到任务返回。
它在内部调用 task.GetAwaiter().GetResult()
,这很简单,但是它重载以处理任何返回 Task
、Task<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()
方法来使用回调方法.
async
”使我的注意力从您的真正意思上移开。
受其他一些答案的启发,我创建了以下简单的辅助方法:
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(是的,有一些罕见的例外)。
return Task.Run(async () => await method()).GetAwaiter().GetResult();
method()
实际上是一个异步方法本身 - 而在@Metalogic 的示例中 foo()
是他异步调用的同步方法。在您的情况下,只需 method().GetAwaiter().GetResult();
就足够了
这是最简单的解决方案。我在网上某处看到过,不记得在哪里,但一直在成功使用。它不会死锁调用线程。
void Synchronous Function()
{
Task.Run(Foo).Wait();
}
string SynchronousFunctionReturnsString()
{
return Task.Run(Foo).Result;
}
string SynchronousFunctionReturnsStringWithParam(int id)
{
return Task.Run(() => Foo(id)).Result;
}
经过几个小时尝试不同的方法,或多或少的成功,这就是我的结局。它在获取结果时不会以死锁结束,它还会获取并抛出原始异常,而不是包装的异常。
private ReturnType RunSync()
{
var task = Task.Run(async () => await myMethodAsync(agency));
if (task.IsFaulted && task.Exception != null)
{
throw task.Exception;
}
return task.Result;
}
斯蒂芬·克利里的回答;
这种方法不应该导致死锁(假设 ProblemMethodAsync 不向 UI 线程或类似的东西发送更新)。它确实假设 ProblemMethodAsync 可以在线程池线程上调用,但情况并非总是如此。
https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
这是方法;
线程池黑客 与阻塞黑客类似的方法是将异步工作卸载到线程池,然后阻塞生成的任务。使用此 hack 的代码将类似于图 7 中所示的代码。 图 7 线程池 Hack 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.NET HttpContext.Current。
这些 Windows 异步方法有一个漂亮的小方法,称为 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()
RunSynchronously()
会导致 InvalidOperationException
。试试这个代码:Task.Run(() => {}).RunSynchronously();
不定期副业成功案例分享
WaitAndUnwrapException
是我自己的 AsyncEx library 方法。官方的 .NET 库没有为混合同步和异步代码提供太多帮助(通常,您不应该这样做!)。在更新 AsyncEx 以在 4.5 上运行之前,我正在等待 .NET 4.5 RTW 和一台新的非 XP 笔记本电脑(我目前无法为 4.5 开发,因为我在 XP 上停留了几个星期)。AsyncContext
现在有一个采用 lambda 表达式的Run
方法,因此您应该使用var result = AsyncContext.Run(() => MyAsyncMethod());
var result = AsyncContext.Run(MyAsyncMethod);
Nito.AsyncEx
库。或者,使用.GetAwaiter().GetResult()
而不是.WaitAndUnwrapException()
。