ASP.NET 4.5 非同步呼叫之射後不理

摘要:這是一個小實驗,看看 ASP.NET 4.5 非同步呼叫如何能夠「射後不理」。

在 ASP.NET 伺服器端程式中採用射後不理的非同步呼叫方式,通常是因為有一些與前端展現無關的工作。這裡有個小實驗,試試射後不理的寫法。

開發工具:Visual Studio 2012

步驟

首先,建立一個 ASP.NET MVC 專案,Target Framework 選擇 .NET Framework 4.5,MVC 範本選擇 Basic。

然後建立一個 HomeController,撰寫 Index 方法:

using System;
using System.Web.Mvc;
using System.Threading.Tasks;

namespace MvcAppAsync.Controllers
{
    public class HomeController : Controller
    {
        public string Index()
        {
            var startTime = DateTime.Now.ToString("HH:mm:ss");

            LogAsync();

            var endTime = DateTime.Now.ToString("HH:mm:ss");
            return String.Format("{0} -- {1}", startTime, endTime);
        }

        private async void LogAsync()
        {
            await Task.Delay(9000); // 刻意延遲九秒.
            string path = System.Environment.GetFolderPath(
                Environment.SpecialFolder.MyDocuments);
            System.IO.File.WriteAllText(path + @"\log.txt", 
                DateTime.Now.ToString("HH:mm:ss"));
        }
    }
}

注意其中的 LogAsync() 是個非同步方法,它會故意延遲約九秒,然後將目前時間寫入使用者的「文件」資料夾底下的 log.txt,例如 C:\Users\Michael\Documents\log.txt。

按 F5 執行此應用程式,你會發現網頁載入時要等待約九秒才顯示網頁內容。可是,網頁上顯示的起始時間和結束時間卻是相同的,若有差距,也不會超過一秒鐘。例如:

00:22:20 -- 00:22:20

再到使用者的「文件」資料夾查看 log.txt 內容,裡面忠實地記錄著時間是 00:22:29。

如此說來,這個非同步方法並沒有加快網頁回應的速度?

的確,就如上一篇筆記中提過的:執行某一件工作所需要花的時間,並不會因為你採用了非同步呼叫的寫法而有顯著差異。畢竟,網頁在返回前端瀏覽器之前,通常都會等所有非同步工作執行完畢,以便取得它們的執行結果。所以,如果將各項非同步工作所花的時間加總起來,其實真正的「總工時」並沒有減少。如果速度有顯著提升,通常是因為非同步呼叫分頭進行網路傳輸或 I/O 作業,讓目前的執行緒能夠繼續處理其他工作。

射後不理

如果呼叫非同步方法時不在乎它的執行結果,也不想等它執行完,亦即所謂的射後不理(fire and forget),那麼以剛才的範例來說,就只要把 LogAsync 方法中的 await 去掉就行了,像這樣:

private async void LogAsync()
{
    Task.Delay(9000); // 把 await 拿掉了.
    string path = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    System.IO.File.WriteAllText(path + @"\log.txt", DateTime.Now.ToString("HH:mm:ss"));
}

再執行看看,這次網頁上顯示的時間和 log.txt 裡面記錄的時間都一樣了,而且網頁開啟時很快就出現內容,不會有延遲九秒的情形。

可是,這寫法有個問題:編譯器會警告你 async 方法中沒有出現對應的 await。原文訊息如下:

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

而且一般也不建議用 async void 方法來實作射後不理的非同步方法

使用 Task.Run()

還有一個方法是使用 Task.Run()。此方法會從 thread pool 中取出一條執行緒來執行你指定的工作。以先前的範例來說,LogAsync() 方法完全不需要 async 和 await 修飾詞,只要在呼叫它的時候透過 Task.Run() 就行了。例如:

Task.Run(() => LogAsync());

此寫法一樣是射後不理。

關於 Task.Run() 方法:
  • 它會使用 thread pool 中的執行緒。
  • 適合用在 CPU-bound 工作,而非 I/O 工作。
  • 是 Task.Factory.StartNew() 的簡便寫法。

沒人處理的 Exception

射後不理的寫法雖然簡單,但還得考慮:這些你理都不想理的工作萬一發生 exception 時怎麼辦?

我在 Windows Server 2008 + .NET 4.5 環境中使用 Task.Run,實驗結果是作業系統會出現 Just-InTime Debugger 對話窗,詢問是否要除錯。在此對話窗結束之前,前端瀏覽器若再嘗試存取這個應用程式,會發生網頁卡住,等很久才取得回應,甚至逾時的狀況。直到 JIT Debugger 對話窗關閉,用戶端才能繼續正常存取網頁。

如果我的理解沒錯,這些從 thread pool 中取出來負責執行背景工作的執行緒如果發生 exception,而且沒人處理的話,.NET 4.0 預設的處理方式是結束目前的 process(應用程式異常終止),而 .NET 4.5 則是自動忽略之。也就是說,對於這種未處理的錯誤,.NET 4.5 會讓應用程式的錯誤容忍度稍微好一些(相對 .NET 4.0 而言),不過這些出問題的工作,還是得等到 CLR 執行資源回收時才得以釋放。(以上敘述如有錯誤還請指正

這種狀況,就算你在程式中攔截 TaskScheduler.UnobservedTaskException  事件也沒用,因為該事件只有在 Task 物件被回收(finalize)時才會觸發。

小結

所以結論是:射後不理是一種不負責任的行為,請三思而後行。

延伸閱讀

技術提供:Blogger.
回頂端⬆️