這樣在 C# 使用 LongRunnigTask 是錯的

2023-03-06 09:00:20

Task.Factory.StartNew 有一個過載,是支援 TaskCreationOptions.LongRunning 引數來指定 Task 的特徵的。但是可能在沒有注意的情況下,你就使用了錯誤的用法。那麼本文我們來簡單闡述一下這個引數的作用,和使用的注意要點。

這樣其實是錯誤的

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

Task.Factory.StartNew(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}, TaskCreationOptions.LongRunning);

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

為什麼需要 LongRunning

我們通常兩種情況下會想到使用 TaskCreationOptions.LongRunning 引數:

  1. 你的任務需要長時間執行,比如一個迴圈,或者一個死迴圈。用來從佇列中取資料,然後處理資料,或者是一些定時任務。
  2. 你的任務需要佔用大量的 CPU 資源,是一個很大的迴圈,比如要遍歷一個很大的陣列,並做一些處理。

那麼這個時候,我們就需要使用 TaskCreationOptions.LongRunning 引數來指定 Task。

因為我們可能學習到了,Task 預設的 Scheduler 是 ThreadPool,而 ThreadPool 的執行緒是有限的,如果你的任務需要長時間執行,或者是需要佔用大量的 CPU 資源,那麼就會導致 ThreadPool 的執行緒不夠用。導致執行緒飢餓,或者是執行緒池的執行緒被佔用,導致其他的任務無法執行。

於是我們很聰明的就想到了,我們可以使用 TaskCreationOptions.LongRunning 引數來指定 Task,這樣就可以避免執行緒飢餓。

弄巧成拙

但是實際上,開篇的寫法並不能達到我們的目的。

我們可以通過以下程式碼來驗證一下:

var task = Task.Factory.StartNew(async () =>
{
    while (true)
    {
        // do something
        await Task.Delay(1000);
    }
}, TaskCreationOptions.LongRunning);

Thread.Sleep(3000);

Console.WriteLine($"Task Status: {task.Status}");
// Task Status: RanToCompletion

我們可以看到,Task 的狀態是並非是 Running,而是 RanToCompletion。

也就是說,我們的任務在 3 秒後就已經執行完了,而不是我們想要的長時間執行。

究其原因,是因為我們採用了非同步的方式來執行任務。而非同步任務的執行,是通過 ThreadPool 來執行的。也就是說,雖然我們使用了 TaskCreationOptions.LongRunning 引數,來想辦法指定執行緒池單獨開一個執行緒,但是實際上在一個 await 之後,我們的任務還是在 ThreadPool 中執行的。

這會導致,我們的任務實際上後續又回到了 ThreadPool 中,而不是我們想要的單獨的執行緒。起不到單獨長期執行的作用。

正確的寫法

因此,實際上如果想要保持單獨的執行緒持續的執行,我們需要移除非同步的方式,改為同步的方式。

var task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        // do something
        Thread.Sleep(1000);
    }
}, TaskCreationOptions.LongRunning);

Thread.Sleep(3000);

Console.WriteLine($"Task Status: {task.Status}");
// Task Status: Running

這樣我們就可以看到,Task 的狀態是 Running,而不是 RanToCompletion。我們通過 TaskCreationOptions.LongRunning 引數,單獨開啟的執行緒就可以一直執行下去。

實際上還有很多考量

要考量 TaskScheduler 的實現

本文采用的是 aspnetcore 的實現,但是在其他的實現中,可能會有不同的實現。你也完全有可能實現一個 await 之後,不回到 ThreadPool 的實現。

LongRunning 也不是就不能用非同步

正如開篇提到的第二種場景,如果你的業務是在第一個 await 之前有大量的同步程式碼,那麼此時單獨開啟一個執行緒,也是有意義的。

我就是一個死迴圈,裡面也是非同步的怎麼辦

那麼你可以考慮讓這個 LongRuning 的 Task,不要 await,而是通過 Wait() 來等待。這樣就可以避免 LongRunning 的 Task 直接結束。

總結

本文我們簡單闡述了 TaskCreationOptions.LongRunning 引數的作用,和使用的注意要點。

參考

感謝閱讀,如果覺得本文有用,不妨點選推薦