不要使用 Dispatcher.Invoke,因為它可能在你的延遲初始化 Lazy 中導致死鎖
阿新 • • 發佈:2018-12-23
WPF 中為了 UI 的跨執行緒訪問,提供了 Dispatcher
執行緒模型。其 Invoke
方法,無論在哪個執行緒呼叫,都可以讓傳入的方法回到 UI 執行緒。
然而,如果你在 Lazy 上下文中使用了 Invoke
,那麼當這個 Lazy<T>
跨執行緒併發時,極有可能導致死鎖。本文將具體說說這個例子。
本文內容
一段死鎖的程式碼
請先看一段非常簡單的 WPF 程式碼:
private Lazy<Walterlv> _walterlvLazy = new Lazy<Walterlv>(() => new Walterlv());
private void OnLoaded(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
// 在後臺執行緒通過 Lazy 獲取。
var backgroundWalterlv = _walterlvLazy.Value;
});
// 等待一個時間,這樣可以確保後臺執行緒先訪問到 Lazy,並且在完成之前,UI 執行緒也能訪問到 Lazy。
Thread.Sleep(50);
// 在主執行緒通過 Lazy 獲取。
var walterlv = _walterlvLazy.Value;
}
而其中的 Walterlv
類的定義也是非常簡單的:
class Walterlv
{
public Walterlv()
{
// 等待一段時間,是為了給我麼的測試程式一個準確的時機。
Thread.Sleep(100);
// Invoke 到主執行緒執行,裡面什麼都不做是為了證明絕不是裡面程式碼帶來的影響。
Application.Current. Dispatcher.Invoke(() =>
{
});
}
}
這裡的 Application.Current.Dispatcher
並不一定必須是 Application.Current
,只要是兩個不同執行緒拿到的 Dispatcher
的例項是同一個,就會死鎖。
此死鎖的觸發條件
Lazy<T>
的執行緒安全引數設定為預設的,也就是LazyThreadSafetyMode.ExecutionAndPublication
;- 後臺執行緒和主 UI 執行緒併發訪問這個
Lazy<T>
,且後臺執行緒先於主 UI 執行緒訪問這個Lazy<T>
; Lazy<T>
內部的程式碼包含主執行緒的Invoke
。
此死鎖的原因
- 後臺執行緒訪問到 Lazy,於是 Lazy 內部獲得同步鎖;
- 主 UI 執行緒訪問到 Lazy,於是主 UI 執行緒等待同步鎖完成,並進入阻塞狀態(以至於不能處理訊息迴圈);
- 後臺執行緒的初始化呼叫到
Invoke
需要到 UI 執行緒完成指定的任務後才會返回,但 UI 執行緒此時阻塞不能處理訊息迴圈,以至於無法完成Invoke
內的任務;
於是,後臺執行緒在等待 UI 執行緒處理訊息以便讓 Invoke
完成,而主 UI 執行緒由於進入 Lazy 的等待,於是不能完成 Invoke
中的任務;於是發生死鎖。
此死鎖的解決方法
Invoke
改為 InvokeAsync
便能解鎖。
這麼做能解決的原因是:後臺執行緒能夠及時返回,這樣 UI 執行緒便能夠繼續執行,包括執行 InvokeAsync
中傳入的任務。
實際上,以上可能是最好的解決辦法了。因為:
- 我們使用 Lazy 並且設定執行緒安全,一定是因為這個初始化過程會被多個執行緒訪問;
- 我們會在 Lazy 的初始化程式碼中使用回到主執行緒的
Invoke
,也是因為我們預料到這份初始化程式碼可能在後臺執行緒執行。
所以,這段初始化程式碼既然不可避免地會併發,那麼就應該阻止併發造成的死鎖問題。也就是不要使用 Invoke
而是改用 InvokeAsync
。
如果需要使用 Invoke
的返回值,那麼改為 InvokeAsync
之後,可以使用 await
非同步等待返回值。
更多死鎖問題
死鎖問題:
- 使用 Task.Wait()?立刻死鎖(deadlock) - walterlv
- 不要使用 Dispatcher.Invoke,因為它可能在你的延遲初始化 Lazy 中導致死鎖 - walterlv
- 在有 UI 執行緒參與的同步鎖(如 AutoResetEvent)內部使用 await 可能導致死鎖
- .NET 中小心巢狀等待的 Task,它可能會耗盡你執行緒池的現有資源,出現類似死鎖的情況 - walterlv
解決方法: