1. 程式人生 > >不要使用 Dispatcher.Invoke,因為它可能在你的延遲初始化 Lazy 中導致死鎖

不要使用 Dispatcher.Invoke,因為它可能在你的延遲初始化 Lazy 中導致死鎖

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 的例項是同一個,就會死鎖。

此死鎖的觸發條件

  1. Lazy<T> 的執行緒安全引數設定為預設的,也就是 LazyThreadSafetyMode.ExecutionAndPublication
  2. 後臺執行緒和主 UI 執行緒併發訪問這個 Lazy<T>,且後臺執行緒先於主 UI 執行緒訪問這個 Lazy<T>
  3. Lazy<T> 內部的程式碼包含主執行緒的 Invoke

此死鎖的原因

  1. 後臺執行緒訪問到 Lazy,於是 Lazy 內部獲得同步鎖;
  2. 主 UI 執行緒訪問到 Lazy,於是主 UI 執行緒等待同步鎖完成,並進入阻塞狀態(以至於不能處理訊息迴圈);
  3. 後臺執行緒的初始化呼叫到 Invoke 需要到 UI 執行緒完成指定的任務後才會返回,但 UI 執行緒此時阻塞不能處理訊息迴圈,以至於無法完成 Invoke 內的任務;

於是,後臺執行緒在等待 UI 執行緒處理訊息以便讓 Invoke 完成,而主 UI 執行緒由於進入 Lazy 的等待,於是不能完成 Invoke 中的任務;於是發生死鎖。

此死鎖的解決方法

Invoke 改為 InvokeAsync 便能解鎖。

這麼做能解決的原因是:後臺執行緒能夠及時返回,這樣 UI 執行緒便能夠繼續執行,包括執行 InvokeAsync 中傳入的任務。

實際上,以上可能是最好的解決辦法了。因為:

  1. 我們使用 Lazy 並且設定執行緒安全,一定是因為這個初始化過程會被多個執行緒訪問;
  2. 我們會在 Lazy 的初始化程式碼中使用回到主執行緒的 Invoke,也是因為我們預料到這份初始化程式碼可能在後臺執行緒執行。

所以,這段初始化程式碼既然不可避免地會併發,那麼就應該阻止併發造成的死鎖問題。也就是不要使用 Invoke 而是改用 InvokeAsync

如果需要使用 Invoke 的返回值,那麼改為 InvokeAsync 之後,可以使用 await 非同步等待返回值。

更多死鎖問題

死鎖問題:

解決方法: