提问人:Tower 提问时间:2/19/2012 最后编辑:Theodor ZouliasTower 更新时间:10/24/2023 访问量:1258512
如何在C#中从同步方法调用异步方法?
How to call asynchronous method from synchronous method in C#?
问:
我有一个方法,我想从同步方法调用它。到目前为止,我从MSDN文档中看到的都是通过方法调用方法,但我的整个程序不是用方法构建的。public async Task Foo()
async
async
async
这甚至可能吗?
下面是从异步方法调用这些方法的一个示例: 演练:使用 Async 和 Await 访问 Web(C# 和 Visual Basic)
现在,我正在研究从同步方法调用这些方法。async
答:
您可以从同步代码调用任何异步方法,也就是说,直到您需要对它们调用为止,在这种情况下,它们也必须被标记为。await
async
正如很多人在这里建议的那样,您可以在同步方法中对生成的任务调用 或 Result,但最终在该方法中会出现阻塞调用,这在某种程度上违背了异步的目的。Wait()
如果你真的无法制作你的方法,并且你不想锁定同步方法,那么你将不得不使用回调方法,将其作为参数传递给任务中的方法。async
ContinueWith()
评论
async
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 方法。最大的区别是逻辑流不会中断,这使得写入和读取变得更加容易。
评论
Wait
包装异常,并有可能发生死锁。
await
myTask.Wait
myTask.RunSynchronously()
T
.Result
Result
Result
Result
异步编程确实通过代码库“增长”。它被比作僵尸病毒。最好的解决方案是让它增长,但有时这是不可能的。
我在 Nito.AsyncEx 库中编写了一些类型,用于处理部分异步代码库。但是,没有一种解决方案适用于所有情况。
解决方案 A
如果你有一个简单的异步方法,不需要同步回其上下文,那么你可以使用:Task.WaitAndUnwrapException
var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();
您不想使用 或 因为它们将异常包装在 中。Task.Wait
Task.Result
AggregateException
仅当不同步回其上下文时,此解决方案才适用。换句话说,每个 in 都应该以 .这意味着它无法更新任何 UI 元素或访问 ASP.NET 请求上下文。MyAsyncMethod
await
MyAsyncMethod
ConfigureAwait(false)
解决方案 B
如果确实需要同步回其上下文,则可以使用来提供嵌套上下文:MyAsyncMethod
AsyncContext.RunTask
var result = AsyncContext.RunTask(MyAsyncMethod).Result;
*2014 年 4 月 14 日更新:在库的较新版本中,API 如下所示:
var result = AsyncContext.Run(MyAsyncMethod);
(在此示例中使用是可以的,因为会传播异常)。Task.Result
RunTask
Task
您可能需要而不是的原因是因为 WinForms/WPF/SL/ASP.NET 上发生相当微妙的死锁可能性:AsyncContext.RunTask
Task.WaitAndUnwrapException
- 同步方法调用异步方法,获取 .
Task
- 同步方法对 执行阻塞等待。
Task
- 该方法不使用 .
async
await
ConfigureAwait
- 在这种情况下无法完成,因为它仅在方法完成时完成;该方法无法完成,因为它正在尝试将其延续计划到 ,并且 WinForms/WPF/SL/ASP.NET 将不允许继续运行,因为同步方法已在该上下文中运行。
Task
async
async
SynchronizationContext
这就是为什么在每种方法中尽可能多地使用是一个好主意的原因之一。ConfigureAwait(false)
async
解决方案 C
AsyncContext.RunTask
并非在所有情况下都有效。例如,如果该方法等待需要 UI 事件才能完成的内容,那么即使使用嵌套上下文,也会死锁。在这种情况下,可以在线程池上启动该方法:async
async
var task = Task.Run(async () => await MyAsyncMethod());
var result = task.WaitAndUnwrapException();
但是,此解决方案需要在线程池上下文中起作用的。因此,它无法更新 UI 元素或访问 ASP.NET 请求上下文。在这种情况下,您不妨添加到其语句中,并使用解决方案 A。MyAsyncMethod
ConfigureAwait(false)
await
更新:Stephen Cleary 撰写的 2015 年 MSDN 文章“异步编程 - 棕地异步开发”。
评论
WaitAndUnwrapException
是我自己的方法,来自我的 AsyncEx 库。官方的 .NET 库没有为混合同步和异步代码提供太多帮助(一般来说,你不应该这样做!我正在等待 .NET 4.5 RTW 和新的非 XP 笔记本电脑,然后再将 AsyncEx 更新为 4.5(我目前无法为 4.5 开发,因为我在 XP 上停留了几个星期)。
AsyncContext
现在有一个接受 lambda 表达式的方法,因此您应该使用Run
var result = AsyncContext.Run(() => MyAsyncMethod());
var result = AsyncContext.Run(MyAsyncMethod);
Nito.AsyncEx
库。或者,使用 代替 ..GetAwaiter().GetResult()
.WaitAndUnwrapException()
我不是 100% 确定,但我相信本博客中描述的技术在许多情况下都应该有效:
因此,如果要直接调用此传播逻辑,则可以使用。
task.GetAwaiter().GetResult()
评论
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));
}
对于那些关心代码许可条款的人,这里有一个指向非常相似的代码的链接(只是在线程上添加了对区域性的支持),其中包含注释以指示它是由 Microsoft 许可的 MIT。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
首先,关闭
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 SynchronizationContext
为了额外的安全性,这样称呼它不是更好吗 这样我们告诉“内部”方法“请不要尝试同步到上层上下文和 dealock”
AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));
alex-from-jitbit 的一点非常好,正如大多数对象架构问题一样,这取决于。
作为扩展方法,您是想对每个调用强制执行该方法,还是让使用该函数的程序员在他们自己的异步调用中配置它?我可以看到调用三个场景的用例;它很可能不是你在 WPF 中想要的东西,在大多数情况下当然是有道理的,但考虑到 ASP.Net Core 中没有上下文,如果你能保证它是 ASP.Net Core 的内部,那么这并不重要。
评论
await
ConfigureAwait(false)
AsyncHelper.RunSync
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://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-topics-subscriptions
评论
MainAsync().Wait()
但是,有一个很好的解决方案可以在(几乎:请参阅注释)每种情况下都有效:即席消息泵 (SynchronizationContext)。
调用线程将按预期被阻止,同时仍确保从异步函数调用的所有延续不会死锁,因为它们将被封送到调用线程上运行的临时 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(...));
有关异步泵的更详细说明,请点击此处。
评论
添加一个最终解决我问题的解决方案,希望可以节省某人的时间。
首先阅读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()
Result
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
Task
Task<T>
Task<HttpResponseMessage>
它看起来像是在阻塞上下文中执行异步代码的 MS 官方方式。似乎对我的用例非常有效。...GetAwaiter().GetResult()
评论
好吧,我多年来一直使用这种方法,它还可以处理和传播来自底层异步任务的异常。它完美无缺。
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();
}
评论
受到其他一些答案的启发,我创建了以下简单的帮助程序方法:
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());
评论
return Task.Run(async () => await method()).GetAwaiter().GetResult();
method()
foo()
method().GetAwaiter().GetResult();
这是最简单的解决方案。我在互联网上的某个地方看到了它,我不记得在哪里,但我已经成功地使用它了。它不会使调用线程死锁。
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)的回答;
这种方法不应该导致死锁(假设 ProblemMethodAsync 不会向 UI 线程或任何内容发送更新 像那样)。它确实假设 ProblemMethodAsync 可以在 线程池线程,但情况并非总是如此。
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。
评论
现在,可以使用源生成器通过同步方法生成器库 (nuget) 创建方法的同步版本。
按如下方式使用它:
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async Task FooAsync()
这将生成可以同步调用的方法。Foo
每个人似乎都以需要等待结果为前提。 我经常需要从我不关心结果的同步方法更新数据。我只是使用丢弃:
_ = UpdateAsync();
评论
async void Foo()
Task
Task