不要使用 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 異步等待返回值。

更多死鎖問題

死鎖問題:

解決方法:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM