async 與 Thread 的錯誤結合

Newbe36524發表於2023-03-13

在 TAP 出現之前,我們可以透過 Thread 來完成一些執行緒操作,從而實現多執行緒和非同步操作。在 TAP 出現之後,有時候為了更高精度的控制執行緒,我們還是會使用到 Thread 。文字講介紹一種錯誤的使用方式,作為讀者的一個參考。

和 TaskCreateOptions.LongRunning 類似

不應該嘗試使用 Thread 執行類似的非同步操作。因為這浪費了開啟執行緒的花銷。

有的時候,你可能會這麼寫:

var thread = new Thread(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}){
    IsBackground = true
};
thread.Start();

但其實,這是個錯誤的寫法。

IDE 提示

和 TaskCreateOptions.LongRunning 略有不同,採用這種寫法,IDE 會給出一個提示,表明希望取消 async 關鍵字。因為實際上

  1. Thread 的所有過載中並沒有支援 Task 相關的過載。
  2. async void 除了在 event handler 中使用,其他地方都是不推薦的。

所以這種做法實際上並不推薦。

而 TaskFactory.StartNew() 的過載中,由於存在一個 Func<T> 的過載,所以導致雖然這種這種使用方式錯誤,卻被 IDE 所接受。

所以這裡其實就可以總結一個簡單的規則:當考察一組 API 是否原生支援 TAP 操作的時候,應該檢視這組 API 中是否存在 Task 相關的過載。如果沒有,那麼說明原生並不能良好支援,如果使用則可能會出現意外的情況

同樣的,當我們自己在設計 API 的時候也應該參考該原則,對於自己希望支援 TAP 的 API,應該提供 Task 相關的過載。

曇花執行緒

在 thread async void 其實上只是一個很小的問題。這個錯誤的關鍵還是造成了一個曇花執行緒。

我們透過以下程式碼來驗證:

var thread = new Thread(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}){
    IsBackground = true
};
thread.Start();

Thread.Sleep(3000);

Console.WriteLine("thread is alive: " + thread.IsAlive);
// thread is alive: False

這裡我們可以看到,thread.IsAlive 的值為 False。這是因為,我們在 thread 中使用了 await 關鍵字,在 await 之後的程式碼,實際上是在另一個 ThreadPool 中的執行緒中執行的。而我們的 thread 本身在 await 之後就已經結束了。於是我們就得到了一個曇花一現的執行緒。

而這種曇花執行緒無疑就是一種浪費。

如何觀測執行緒的生命週期

其實大體的內容我們已經講完了。但為了湊一下篇幅,我們著重演示一下如何使用 Rider 來觀測執行緒的生命週期。

首先我們在 Rider 中建立一個單元測試專案,然後在其中建立一個單元測試:

[Test]
public void Test1()
{
    var t1 = new Thread(async () =>
    {
        while (true)
        {
            // do something
            await Task.Delay(1000);
        }
    })
    {
        IsBackground = true,
        Name = "t1"
    };
    t1.Start();

    var t2 = new Thread(() =>
    {
        while (true)
        {
            // do something
            Thread.Sleep(1000);
        }
    })
    {
        IsBackground = true,
        Name = "t2"
    };
    t2.Start();

    Thread.Sleep(3000);
}

然後我們在 Rider 中按照下圖選擇 Profile 選項:

profile1

然後選擇 Profile Unit Tests 選項:

profile2

稍等片刻之後,我們就可以雙擊下圖中的報告,來檢視執行緒的生命週期:

profile3

在檢視介面中,我們可以透過執行緒下來框來檢視執行緒執行所花費的時間:

profile4

如果上圖,我們可以很直接的看到,t1 執行緒的生命週期可以說是瞬間就結束了,因為第一次 await 之後,執行緒就結束了。

總結

在本文中,我們演示了一種錯誤的使用方式,以及如何使用 Rider 來觀測執行緒的生命週期。

參考

感謝閱讀,如果覺得本文有用,不妨點選推薦?或者在評論區留下 Mark,讓更多的人可以看到。

歡迎關注作者的微信公眾號“newbe技術專欄”,獲取更多技術內容。 關注微信公眾號“newbe技術專欄”


  1. https://www.cnblogs.com/eventhorizon/p/15912383.html

  2. https://threads.whuanle.cn/3.task/

  3. https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcreationoptions?view=net-7.0&WT.mc_id=DX-MVP-5003606

相關文章