的行為Task.Wait()出乎意料地不同,具體取決于呼叫的“環境”。在應用程式啟動期間使用以下異步方法
呼叫會通過(不會導致死鎖),而從 WPF 按鈕處理程式中呼叫相同的代碼會阻塞。Task.Wait()TestAsync
重現步驟:
在 Visual Studio 中,使用向導創建一個普通的 WPF .NET 框架應用程式(例如,命名為WpfApp)。
在App.xaml.cs應用程式檔案的檔案中粘貼下面的Main方法和TestAsync方法。
在專案屬性中設定Startup object為WpfApp.App.
在App.xamlswitch Build Actionfrom ApplicationDefinitionto的屬性中Page。
public partial class App : Application
{
[STAThread]
public static int Main(string[] args)
{
Task<DateTime> task = App.TestAsync();
task.Wait();
App app = new App();
app.InitializeComponent();
return app.Run();
}
internal static async Task<DateTime> TestAsync()
{
DateTime completed = await Task.Run<DateTime>(() => {
System.Threading.Thread.Sleep(3000);
return DateTime.Now;
});
System.Diagnostics.Debug.WriteLine(completed);
return completed;
}
}
Observe that the application starts properly (after 3sec delay) and that the "completed" DateTime is written to debug output.
Next create a Button in MainWindow.xaml with Click handler Button_Click in MainWindow.xaml.cs
public partial class MainWindow : Window
{
...
private void Button_Click(object sender, RoutedEventArgs e)
{
Task<DateTime> task = App.TestAsync();
task.Wait();
}
}
Observe that after clicking the Button, the application is deadlocked.
Why can't it pass in both cases?
Is there a way to change invocation (e.g. using ConfigureAwait at the correct task or somehow setting SynchronizationContext or whatever) so that it behaves identical in both invocations, but still synchronously waits for completion?
Update on limitations of the solution.
The async method like TestAsync comes from a library that cannot be changed.
The invocation code of the TestAsync method is nested within a callstack that cannot be changed either, and the code outside the callstck makes use of the returned value of the async method.
Ultimately the solution code has to convert the async method to run synchronous by not changing the method nor the caller.
This works well within UT code (NUnit) and during application startup, but no more within a handler of WPF.
Why?
uj5u.com熱心網友回復:
有幾種不同的方法可以處理這種情況,但最終在一種情況下出現死鎖而不是另一種情況的原因是在 Main 方法中呼叫時SynchronizationContext.Current為 null,因此沒有主 UI 背景關系捕獲和所有異步回呼都在執行緒池執行緒上處理。當從按鈕呼叫時,會自動捕獲一個同步背景關系,因此這種情況下的所有異步回呼都在導致死鎖的主 UI 執行緒上處理。通常,您不會出現死鎖的唯一方法是強制異步代碼不捕獲同步背景關系,或者一直使用異步并且不要從主 UI 背景關系同步等待。
- 您可以
ConfigureAwait(false)在您的TestAsync方法內部,這樣它就不會捕獲同步背景關系并嘗試在主 UI 執行緒上繼續(這最終是導致死鎖的原因,因為您正在呼叫task.Wait()阻塞 UI 執行緒的 UI 執行緒,并且您System.Diagnostics.Debug.WriteLine(completed);嘗試將其安排回 UI 執行緒,因為 await 會自動捕獲同步背景關系)
DateTime completed = await Task.Run<DateTime>(() => {
System.Threading.Thread.Sleep(3000);
return DateTime.Now;
}).ConfigureAwait(false);
- 您可以在后臺執行緒上啟動異步任務,這樣就沒有要捕獲的同步背景關系。
private void Button_Click(object sender, RoutedEventArgs e)
{
var task = Task.Run(() => App.TestAsync());
var dateTime = task.Result;
}
- 您可以在整個堆疊中使用異步
private async void Button_Click(object sender, RoutedEventArgs e)
{
Task<DateTime> task = App.TestAsync();
var dateTime = await task;
}
- 鑒于你是如何使用它的,如果你不必等到任務完成,你可以放手它最終會完成,但你會失去處理任何例外的背景關系
private void Button_Click(object sender, RoutedEventArgs e)
{
//assigning to a variable indicates to the compiler that you
//know the application will continue on without checking if
//the task is finished. If you aren't using the variable, you
//can use the throw away special character _
_ = App.TestAsync();
}
These options are not in any particular order, and actually, best practice would probably be #3. async void is allowed specifically for cases like this where you want to handle a callback event asynchronously.
uj5u.com熱心網友回復:
據我了解,在 .NET 中,許多前端都有一個 UI 執行緒,因此必須一直異步撰寫。其他執行緒被保留并用于渲染之類的事情。
對于 WPF,這就是為什么使用Dispatcher以及如何排隊作業項很重要,因為這是您與您可以使用的一個執行緒進行互動的方式。更多閱讀在這里
丟棄,.Result因為這會阻塞,將方法重寫為異步,并從內部呼叫它Dispatch.Invoke(),它應該按預期運行
uj5u.com熱心網友回復:
為什么這兩種情況都不能通過?
不同之處在于存在SynchronizationContext. 所有執行緒開始時都沒有SynchronizationContext. UI 應用程式有一個特殊的 UI 執行緒,有時他們需要創建一個SynchronizationContext并將其安裝在該執行緒上。發生這種情況的確切時間沒有記錄(或一致),但它必須在 UI 主回圈開始時安裝。
在這種情況下,WPF 將(最遲)在對Application.Run. 來自 UI 框架(例如,事件處理程式)的所有用戶呼叫都發生在此背景關系中。
阻塞代碼與背景關系發生死鎖,因為這是典型的死鎖情況,需要三個組件:
- 一次只允許一個執行緒的背景關系。
- 捕獲該背景關系的異步方法。
- 一個也在該背景關系中運行的方法,該方法會阻塞等待該異步方法。
在 WPF 代碼安裝背景關系之前,條件 (1) 沒有得到滿足,這就是它沒有死鎖的原因。
有沒有辦法改變呼叫(例如在正確的任務中使用 ConfigureAwait 或以某種方式設定 SynchronizationContext 或其他),以便它在兩個呼叫中的行為相同,但仍同步等待完成?
我們-呃...
這是對“我如何阻止異步代碼”的改寫,對此沒有好的答案。最好的答案是根本不阻塞異步代碼;即,async一路使用。特別是因為這是GUI 代碼,我想說為了 UX,你真的想避免阻塞。由于您使用的是 WPF,因此您可能會發現異步 MVVM 資料系結之類的技術很有用。
也就是說,如果必須的話,您可以使用一些技巧。使用ConfigureAwait是一種可能的解決方案,但不是我推薦的;您必須將其應用于所有await被阻止的方法的傳遞閉包中的所有 s (Blocking Hack)。或者,您可以將作業分流到執行緒池 ( Task.Run) 并阻止該執行緒池 (Thread Pool Hack)。或者您可以洗掉SynchronizationContext- 除非被阻止的代碼操縱 UI 元素或系結資料。或者還有更危險的黑客,我真的不能推薦(嵌套訊息回圈黑客)。
但即使在為 hack 付出了所有努力之后,你最終還是會阻塞 UI。黑客之所以很難,正是因為不推薦使用它們。給你的用戶帶來更糟糕的體驗是一項相當多的作業。遠遠更好的解決方案(對于您的用戶和未來的代碼維護者)是一路異步。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/452452.html
