[英]How to call asynchronous method from synchronous method in C#?
我有一個public async void Foo()
方法,我想從同步方法調用它。 到目前為止,我從 MSDN 文檔中看到的都是通過異步方法調用異步方法,但我的整個程序並不是使用異步方法構建的。
這可能嗎?
下面是從異步方法調用這些方法的一個示例:
演練:使用 Async 和 Await(C# 和 Visual Basic)訪問 Web
現在我正在研究從同步方法調用這些異步方法。
異步編程確實通過代碼庫“增長”。 它被比作僵屍病毒。 最好的解決方案是讓它增長,但有時這是不可能的。
我在Nito.AsyncEx庫中編寫了一些類型來處理部分異步代碼庫。 但是,沒有在所有情況下都有效的解決方案。
方案一
如果您有一個不需要同步回其上下文的簡單異步方法,那么您可以使用Task.WaitAndUnwrapException
:
var task = MyAsyncMethod();
var result = task.WaitAndUnwrapException();
你不希望使用Task.Wait
或Task.Result
因為包裝在異常AggregateException
。
此解決方案僅適用於MyAsyncMethod
不同步回其上下文的情況。 換句話說, MyAsyncMethod
每個await
MyAsyncMethod
應該以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
。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();
但是,此解決方案需要一個將在線程池上下文中工作的MyAsyncMethod
。 因此它無法更新 UI 元素或訪問 ASP.NET 請求上下文。 在這種情況下,您也可以將ConfigureAwait(false)
添加到其await
語句中,並使用解決方案 A。
更新,2019 年 5 月 1 日:當前的“最壞做法”在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 基類只有 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()).Result 一樣嗎? AFAIK,微軟現在不鼓勵調用 TaskFactory.StartNew,因為它們都是等價的,並且一個比另一個更易讀。
絕對不。
簡單的答案是
.Unwrap().GetAwaiter().GetResult() != .Result
首先關閉
Task.Result 和 .GetAwaiter.GetResult() 一樣嗎?
其次.Unwrap()使 Task 的設置不會阻塞包裝的任務。
這應該導致任何人問
這不是和調用 Task.Run(async ()=> await AsyncFunc()).GetAwaiter().GetResult() 一樣嗎
這將是一個It Depends 。
關於 Task.Start() 、 Task.Run() 和 Task.Factory.StartNew() 的使用
摘抄:
Task.Run 使用 TaskCreationOptions.DenyChildAttach 這意味着子任務不能附加到父級,它使用 TaskScheduler.Default 這意味着在線程池上運行任務的將始終用於運行任務。
Task.Factory.StartNew 使用 TaskScheduler.Current 表示當前線程的調度程序,它可能是TaskScheduler.Default 但不總是。
補充閱讀:
為了額外的安全,像這樣調用它不是更好
AsyncHelper.RunSync(async () => await AsyncMethod().ConfigureAwait(false));
通過這種方式,我們告訴“內部”方法“請不要嘗試同步到上層上下文並解除鎖定”
非常重要的一點,正如大多數對象架構問題一樣,這取決於。
作為一個擴展方法你想使用的功能配置給力,對於絕對每一個電話,或者你讓程序員認為自己的異步調用? 我可以看到調用三個場景的用例; 它很可能不是您在 WPF 中想要的東西,在大多數情況下當然是有意義的,但是考慮到 ASP.Net Core 中沒有Context,如果您可以保證它是 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 : //docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-dotnet-how-to-use-主題訂閱
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 方法。 最大的區別是邏輯流不會被中斷,這使得讀寫變得更加容易。
我不是 100% 確定,但我相信本博客中描述的技術應該適用於許多情況:
因此,如果您想直接調用此傳播邏輯,則可以使用
task.GetAwaiter().GetResult()
。
然而,有一個很好的解決方案(幾乎:見評論)適用於每種情況:一個特別的消息泵(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(...));
此處提供了異步泵的更詳細說明。
對於任何關注這個問題的人......
如果您查看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()
方法來使用回調方法。
受其他一些答案的啟發,我創建了以下簡單的輔助方法:
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 (是的,有一些罕見的例外)。
Stephen Cleary 的回答;
該方法不應導致死鎖(假設 ProblemMethodAsync 不會將更新發送到 UI 線程或類似的東西)。 它確實假定可以在線程池線程上調用 ProblemMethodAsync,但情況並非總是如此。
https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
這是方法;
線程池 Hack 與 Blocking Hack 類似的方法是將異步工作卸載到線程池,然后阻塞生成的任務。 使用此 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。
我知道我來晚了。 但是,如果像我這樣的人想要以一種整潔,簡單的方式解決此問題,而又不必依賴其他庫。
public static class AsyncHelpers
{
private static readonly TaskFactory taskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
/// <summary>
/// Executes an async Task method which has a void return value synchronously
/// USAGE: AsyncUtil.RunSync(() => AsyncMethod());
/// </summary>
/// <param name="task">Task method to execute</param>
public static void RunSync(Func<Task> task)
=> taskFactory
.StartNew(task)
.Unwrap()
.GetAwaiter()
.GetResult();
/// <summary>
/// Executes an async Task<T> method which has a T return type synchronously
/// USAGE: T result = AsyncUtil.RunSync(() => AsyncMethod<T>());
/// </summary>
/// <typeparam name="TResult">Return Type</typeparam>
/// <param name="task">Task<T> method to execute</param>
/// <returns></returns>
public static TResult RunSync<TResult>(Func<Task<TResult>> task)
=> taskFactory
.StartNew(task)
.Unwrap()
.GetAwaiter()
.GetResult();
}
那么你可以這樣稱呼它
var t = AsyncUtil.RunSync<T>(() => AsyncMethod<T>());
經過數小時嘗試不同的方法,或多或少的成功,這就是我的結局。 在獲得結果時它不會以死鎖結束,它還會獲取並拋出原始異常而不是包裝的異常。
private ReturnType RunSync()
{
var task = Task.Run(async () => await myMethodAsync(agency));
if (task.IsFaulted && task.Exception != null)
{
throw task.Exception;
}
return task.Result;
}
這是最簡單的解決方案。 我在網上的某個地方看到過,不記得在哪里了,但我一直在成功使用它。 它不會死鎖調用線程。
void Synchronous Function()
{
Task.Run(Foo).Wait();
}
string SynchronousFunctionReturnsString()
{
return Task.Run(Foo).Result;
}
string SynchronousFunctionReturnsStringWithParam(int id)
{
return Task.Run(() => Foo(id)).Result;
}
這些 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
// .....
}
希望這可以幫助!
//Example from non UI thread -
private void SaveAssetAsDraft()
{
SaveAssetDataAsDraft();
}
private async Task<bool> SaveAssetDataAsDraft()
{
var id = await _assetServiceManager.SavePendingAssetAsDraft();
return true;
}
//UI Thread -
var result = Task.Run(() => SaveAssetDataAsDraft().Result).Result;
如果你想運行它同步
MethodAsync().RunSynchronously()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.