public async Task Foo()
的方法,我想从一个同步方法中调用它。到目前为止,我在MSDN文档中看到的只有通过async
方法调用async
方法的方法,但我的整个程序并没有构建在async
方法上。这种情况是否可能?
这里有一个从异步方法中调用这些方法的示例:
通过使用Async和Await访问Web的演练(C#和Visual Basic) 现在我正在研究如何从同步方法中调用这些
async
方法。public async Task Foo()
的方法,我想从一个同步方法中调用它。到目前为止,我在MSDN文档中看到的只有通过async
方法调用async
方法的方法,但我的整个程序并没有构建在async
方法上。async
方法。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;
var result = AsyncContext.Run(MyAsyncMethod);
Task.Result
是可以的,因为 RunTask
将传播 Task
异常)。AsyncContext.RunTask
而不是 Task.WaitAndUnwrapException
的原因是基于 WinForms/WPF/SL/ASP.NET 存在一种相当微妙的死锁可能性:
Task
。Task
进行阻塞等待。async
方法使用 await
而没有使用 ConfigureAwait
。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();
WaitAndUnwrapException
是我自己的方法,来自于我的 AsyncEx 库。官方的 .NET 库对于混合使用同步和异步代码提供的帮助不多(通常情况下,你不应该这样做!)。我在等待 .NET 4.5 RTW 和一台新的非 XP 笔记本电脑,然后才会更新 AsyncEx 以在 4.5 上运行(目前我无法为 4.5 进行开发,因为我还需要几个星期继续使用 XP)。 - Stephen ClearyAsyncContext
有一个接受lambda表达式的Run
方法,因此您应该使用var result = AsyncContext.Run(() => MyAsyncMethod());
。 - Stephen Clearyvar result = AsyncContext.Run(MyAsyncMethod);
。更多详情请参考这个链接:http://nitoasyncex.codeplex.com/wikipage?title=AsyncContext。 - Stephen ClearyNito.AsyncEx
库。 或者使用 .GetAwaiter().GetResult()
替代 .WaitAndUnwrapException()
。 - Stephen Cleary分享一个最终解决我的问题的方法,希望能够节省某人的时间。
首先阅读几篇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()
不是最佳实践。但是,再次强调,对于原始问题的答案是什么呢?永远不要同步调用异步方法吗?我们希望这样做,但在现实世界中,有时我们不得不这么做。 - TohidResult
属性由原始上下文获取结果。在第二种情况下,任务在同一上下文中执行,如果该上下文为UI线程,将导致死锁。 - AsPasinternal 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();
}
}
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));
}
.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,这意味着总是使用线程池来运行任务。AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));
这样我们告诉"内部"方法"请不要尝试与上层上下文同步并导致死锁"。
通过alex-from-jitbit提出的观点非常好,就大多数对象架构问题而言都是因情况而异。
作为一个扩展方法,你是否想要强制对每次调用都进行配置,还是让程序员在使用函数时自行配置异步调用?我可以看到有三种调用场景的用例;在 WPF 中可能不是你想要的,但在大多数情况下是有意义的,但考虑到 ASP.Net Core 中没有 Context,如果你能保证它在 ASP.Net Core 中是 say internal 的,那就无所谓了。
await
调用中都没有使用ConfigureAwait(false)
修饰符。我尝试使用AsyncHelper.RunSync
从Global.asax的Application_Start()函数调用异步函数,似乎可以工作。这是否意味着AsyncHelper.RunSync
可靠地不容易出现在本帖子中其他地方所提到的“返回调用者上下文”的死锁问题? - Bob.at.Indigo.HealthAsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));
这样我们告诉“内部”方法“请不要尝试同步到上下文并发生死锁”。 - Alex from Jitbitasync Main 现在是 C# 7.2 的一部分,可以在项目的高级构建设置中启用。
对于 C# < 7.2,正确的方式是:
static void Main(string[] args)
{
MainAsync().GetAwaiter().GetResult();
}
static async Task MainAsync()
{
/*await stuff here*/
}
你会在很多微软文档中看到这个被使用,例如:https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions
MainAsync().Wait()
更好? - crush我不确定,但我认为在这篇博客中所描述的技术,在许多情况下都能起作用:
如果你想直接调用这种传播逻辑,你可以使用
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 后面的代码类似于以前的回调函数。主要的区别是逻辑流不会被中断,这使得编写和阅读更加容易。
Wait()
更改为Result
,因为这也会阻塞直到任务完成。 - Despertar.Result
时卡住了。 - iCollect.it Ltd然而,有一个解决方案在几乎所有情况下都可以使用:即时消息泵(同步上下文)。
调用线程将像预期的那样被阻塞,同时确保从异步函数调用的所有继续调用不会死锁,因为它们将被编组到运行在调用线程上的临时同步上下文(消息泵)中。
即时消息泵助手的代码:
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()
,这非常简单,但是它被重载以适用于返回 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
”分散了我的注意力,让我无法理解你真正想表达的意思。 - Jeff Mercado
async void Foo()
方法没有返回Task
,这意味着调用者无法知道它何时完成,必须改为返回Task
。 - Dai