2018年8月22日 星期三

在 ASP.NET 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題 HttpContext

在 ASP.NET 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題 HttpContext

SynchronizationContext類別是一個基底類別,它提供執行緒內容的同步處理需求,它可以幫助我們在在不同的同步情況下,進行進行非同步/同步操作正確行為。若想要更深入去了解 SynchronizationContext 這個類別功能,可以參考 不可或缺的 SynchronizationContext 文章。
在這篇文章中,我們將要來說明在 ASP.NET 使用 執行緒的同步內容 SynchronizationContext 進行非同步工作處理問題,也就是我們在 ASP.NET 專案程式碼開發時候,當使用者發出一個 Http Request 請求的時候, IIS 將會從執行緒集區取出一個執行緒,用來處理這個使用者請求;此時,在這個執行緒中,當時的同步內容 SynchronizationContext 將會是一個 System.Web.AspNetSynchronizationContext 類別物件,這個類別物件將會用於處理 ASP.NET 的執行緒同步內容的處理工作。因此,若我們在這個 Http Request 請求執行緒中,另外進行一個非同步的工作,而該非同步的工作將會在另外一個新的執行緒中來執行,此時,在這個新的執行緒中,將無法取得 System.Web.HttpContext.Current 屬性值,此時,該屬性值將會是 null。
另外,當非同步工作完成之後,若沒有指定返回的執行緒要進行同步內容切換,則會造成呼叫非同步工作後的相關程式碼,也無法取得 System.Web.HttpContext.Current 屬性值,因此,若沒有遵守這樣的要求,將會造成您的應用程式發生例外異常的問題。這樣的問題將會發生在您的 ASP.NET 專案中有使用到 多執行緒 Multiple Thread 或者 非同步 Asynchronous 處理需求,若沒有特別注意,將會發生不可預期的例外異常問題。
我們來展示當您進行多執行緒或者非同步工作程式設計並且沒有遵循這樣要求,會發生甚麼樣的問題,以及要如何解決此一問題。在這裡您需要知道 ASP.NET 有繼承 SynchronizationContext 類別,實作出 System.Web.AspNetSynchronizationContext 類別,當您在 Http Request 請求執行緒下執行程式碼,此時,需要存取 System.Web.HttpContext.Current 屬性值,您可以透過 AspNetSynchronizationContext 物件幫助您做到同步內容,如此,也就是不會發生不明例外異常。
底下是我們要測試的 HomeController 程式碼。
首先,我們來看看當 continueOnCapturedContext = true 的執行結果
在這個測試中, Index 動作方法將會等候 Task.Run 這個非同步執行完成,而且,在這個非同步工作的等候前後,我們都可以看到當時的 Http Context 是都有值的,他的型別為 System.Web.AspNetSynchronizationContext。不過,在 Tas.Run 內的委派方法,在進行 Thread.Sleep 方法呼叫前後,我們是都無法捕捉到任何關於 System.Web.HttpContext.Current 的屬性值,這是因為要等候 Task.Run 這個非同步工作的時候,該非同步工作裡面會產生一個新的執行緒 (此執行緒 ID 為 9),而對於當使用者請求這個 Http Request 請求時候所用的執行緒 (此執行緒 ID 為 8) 是不同的,因此,在新的執行緒中,是無法讀取到任何 System.Web.HttpContext.Current 的屬性值。
不過,由於我們有在 Task.Run 這個非同步方法之後,又執行了 ConfigureAwait(true) 這個方法,所以,當非同步工作執行完成之後,將會透過執行緒同步內容,也就是 System.Web.AspNetSynchronizationContext 類別的物件,將原先 Http Request 請求的相關內容,複製到這個從非同步工作返回的執行緒中,故,我們可以還是可以看到與存取 System.Web.HttpContext.Current 的屬性值。在這裡您會看到,一開始使用者 Http 請求的執行緒為 ID=8,非同步工作的執行緒 ID=9,不過,這裡不像是 WPF 類型的應用程式,在 ASP.NET 專案程式中,在完成非同步工作之後,將會從執行緒集區 ThreadPool 內,取得一個新的執行緒,透過同步內容物件的幫助,還原 System.Web.HttpContext.Current ,因此,我們看到當完成非同步工作的執行緒,是與當初請求的執行緒不同,但是,還是可以繼續存取 System.Web.HttpContext.Current 的屬性值。
進行 System.Web.HttpContext.Current 多執行緒的同步內文測試
執行緒 Thread → 8
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext → System.Web.AspNetSynchronizationContext
Http內文 Http Context → System.Web.HttpContext


等候 非同步工作 完成
執行緒 Thread → 8
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext → System.Web.AspNetSynchronizationContext
Http內文 Http Context → System.Web.HttpContext


正在非同步工作中 / Sleep 方法將會被呼叫
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext →
Http內文 Http Context →

準備結束非同步工作 / 是否要回到原先執行緒的同步內文中 True
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext →
Http內文 Http Context →

非同步工作 已經完成
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext → System.Web.AspNetSynchronizationContext
Http內文 Http Context → System.Web.HttpContext
ASP.NET SynchronizationContext
現在,我們來看看當 continueOnCapturedContext = false 的執行結果
在這個測試中, Index 動作方法將會等候 Task.Run 這個非同步執行完成,而且,在這個非同步工作的等候前後,我們都可以看到當時的 Http Context 是都有值的,他的型別為 System.Web.AspNetSynchronizationContext。不過,在 Tas.Run 內的委派方法,在進行 Thread.Sleep 方法呼叫前後,我們是都無法捕捉到任何關於 System.Web.HttpContext.Current 的屬性值,這是因為要等候 Task.Run 這個非同步工作的時候,該非同步工作裡面會產生一個新的執行緒 (此執行緒 ID 為 9),而對於當使用者請求這個 Http Request 請求時候所用的執行緒 (此執行緒 ID 為 8) 是不同的,因此,在新的執行緒中,是無法讀取到任何 System.Web.HttpContext.Current 的屬性值。
不過,由於我們有在 Task.Run 這個非同步方法之後,又執行了 ConfigureAwait(false) 這個方法,因此,當非同步工作執行完成之後,將會不會執行執行緒同步內容工作,將原先 Http Request 請求的相關內容,複製到這個從非同步工作返回的執行緒中,故,我們是無法看到與存取 System.Web.HttpContext.Current 的屬性值。在這裡您會看到,一開始使用者 Http 請求的執行緒為 ID=8,非同步工作的執行緒 ID=9,不過,這裡不像是 WPF 類型的應用程式,在 ASP.NET 專案程式中,在完成非同步工作之後,將會從執行緒集區 ThreadPool 內,取得一個新的執行緒,透過同步內容物件的幫助,還原 System.Web.HttpContext.Current ,因此,我們看到當完成非同步工作的執行緒,是與當初請求的執行緒不同,但是,還是可以繼續存取 System.Web.HttpContext.Current 的屬性值。
進行 System.Web.HttpContext.Current 多執行緒的同步內文測試
執行緒 Thread → 8
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext → System.Web.AspNetSynchronizationContext
Http內文 Http Context → System.Web.HttpContext


等候 非同步工作 完成
執行緒 Thread → 8
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext → System.Web.AspNetSynchronizationContext
Http內文 Http Context → System.Web.HttpContext


正在非同步工作中 / Sleep 方法將會被呼叫
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext →
Http內文 Http Context →

準備結束非同步工作 / 是否要回到原先執行緒的同步內文中 False
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext →
Http內文 Http Context →

非同步工作 已經完成
執行緒 Thread → 9
工作排程 Task scheduler → System.Threading.Tasks.ThreadPoolTaskScheduler
同步內文 SynchronizationContext →
Http內文 Http Context →
ASP.NET SynchronizationContext
C Sharp / C#
public class HomeController : Controller
{
    public async Task<ActionResult> Index()
    {
        // 是否要回到原先執行緒的同步內文中
        // https://msdn.microsoft.com/zh-tw/library/system.threading.tasks.task.configureawait(v=vs.110).aspx
        bool continueOnCapturedContext = true;
        Response.Write($"{Log("進行 System.Web.HttpContext.Current 多執行緒的同步內文測試")}<br/><hr/><br/>");
        try
        {
            Response.Write($"{Log("等候 非同步工作 完成")}<br/><hr/><br/>");

            // 使用我們指定的條件,進行呼叫非同步方法
            await Task.Run(async () =>
            {
                Response.Write($"{Log($"正在非同步工作中 / Sleep 方法將會被呼叫")}<br/>");
                // 要休息 1000 毫秒數
                System.Threading.Thread.Sleep(1000);
                Response.Write($"{Log($"準備結束非同步工作 / 是否要回到原先執行緒的同步內文中 <strong>{continueOnCapturedContext.ToString()}</strong>")}<br/>");
            }).ConfigureAwait(continueOnCapturedContext);

            Response.Write($"{Log("非同步工作 已經完成")}<br/><hr/>");
        }
        catch (Exception e)
        {
            Response.Write($"{Log($"Error {e.Message}")}<br/>");
        }

        return new HttpStatusCodeResult(200);
    }

    string Log(string msg)
    {
        var synchronizationContext = System.Threading.SynchronizationContext.Current;
        var taskScheduler = TaskScheduler.Current;
        var httpContextCurrent = System.Web.HttpContext.Current;
        return $"{msg} <ul><li>" +
            $"執行緒 Thread → {System.Threading.Thread.CurrentThread.ManagedThreadId} </li><li>" +
            $"工作排程 Task scheduler → {taskScheduler} </li><li>" +
            $"同步內文 SynchronizationContext → {synchronizationContext} </li><li>" +
            $"Http內文 Http Context → {httpContextCurrent} </li></ul>";
    }

    public ActionResult About()
    {
        ViewBag.Message = "Your application description page.";

        return View();
    }

    public ActionResult Contact()
    {
        ViewBag.Message = "Your contact page.";

        return View();
    }
}

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程



沒有留言:

張貼留言