﻿# 利用 SynchronizationContext 讓 ASP.NET 背景執行緒取用 HttpContext 資訊

我們在寫 ASP.NET (.NET Framework) 的時候，可能會需要利用非同步作業在背景執行一些工作，同時可能也需要偶爾取用 `HttpContext` 相關資訊。事實上當我們在 ASP.NET (.NET Framework) 使用 `await` 等待一個 Task 時，會自動紀錄當下的 `SynchronizationContext`，並在 Task 完成工作後，取得原始 **執行緒** (Thread) 中的資訊。但是如果我們也想在 Task 中取用原本執行緒的內容 (例如 `HttpContext` 資訊)，我們就需要學會如何自行利用 `SynchronizationContext` 來取用原始執行緒中的內容。今天這篇文章，我將透過一段簡易的程式碼與一個實際應用情境，說明 `SynchronizationContext` 的運作方式，讓你知道如何正確的使用它。

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/b2b054c407fe31a908f8-174488748-4341d075-2700-40c6-9cdb-269c8711f6bf.jpg)

以下我先寫一段虛擬碼 (Pseudocode)，假設我們想執行一段沒有支援非同步 API 的程式碼，這段程式碼由於資料處理的執行時間過長，有必要改寫為「非同步」的版本，以增加執行緒使用效率：

```csharp
private static async Task<IEnumerable<WeatherForecast>> NewMethodAsync()
{
    // 假設我們想執行一段沒有支援非同步 API 的程式碼
    var result = await Task.Run(() =>
    {
        // 取得大量資料
        var data = repository.GetAll();

        int counter = 1;
        foreach (var item in data)
        {
            // 這裡的 System.Web.HttpContext.Current 將會得到 null 空值，程式將會發生錯誤！
            if (counter % 1000 == 0 && !System.Web.HttpContext.Current.Response.IsClientConnected)
            {
                log.Information("Client Disconnected");
                break;
            }

            // TODO: 對大量資料進行加工處理

            counter++;
        }

        // 回傳處理過的資料
        return data;
    });

    return result;
}
```

上述程式碼，你將無法在非同步執行的 `Task.Run()` 裡面使用 `System.Web.HttpContext.Current` 屬性，因為 `Task.Run()` 在執行的時候，會透過 ThreadPool 取得一個**背景執行緒**來執行程式碼，該執行緒並沒有原本 `HttpContext` 的內容，因此無法得到預期的結果，而且程式將會發生例外狀況。

此時，我們就可以在 Task 開始執行之前，利用 `SynchronizationContext.Current` 取得一個**同步內容**物件，搭配 C# 的 Closure (閉包) 特性，將該物件帶到 `Task.Run()` 裡面使用。之後你就可以透過 [Post()](https://learn.microsoft.com/zh-tw/dotnet/api/system.threading.synchronizationcontext.post?view=net-5.0&WT.mc_id=DT-MVP-4015686) 或 [Send()](https://learn.microsoft.com/zh-tw/dotnet/api/system.threading.synchronizationcontext.send?view=net-5.0&WT.mc_id=DT-MVP-4015686) 方法，將 [SendOrPostCallback](https://learn.microsoft.com/en-us/dotnet/api/system.threading.sendorpostcallback?view=net-5.0&WT.mc_id=DT-MVP-4015686) 委派帶到 `SynchronizationContext` 同步內容中執行。

你可以這樣想像，所有透過 [SendOrPostCallback](https://learn.microsoft.com/en-us/dotnet/api/system.threading.sendorpostcallback?view=net-5.0&WT.mc_id=DT-MVP-4015686) 委派帶到 `SynchronizationContext` 同步內容中執行的程式碼，都可以順利的獲取原先從 `SynchronizationContext.Current` 儲存下來的執行緒內容(`Thread.CurrentThread`)，並藉此取得完整的 `System.Web.HttpContext.Current` 屬性值。如此一來你就可以順利取得 `System.Web.HttpContext.Current.Response.IsClientConnected` 屬性內容，在**背景執行緒**中取得目前用戶端是否已經斷線，如果斷線就立即中斷資料處理作業，以節省 CPU 資源耗用。

以下是修改過後的程式碼：

```csharp
private static async Task<IEnumerable<WeatherForecast>> NewMethodAsync()
{
    // 先將 SynchronizationContext.Current 保存下來
    var sc = SynchronizationContext.Current;

    var result = await Task.Run(() =>
    {
        // 取得大量資料
        var data = repository.GetAll();

        // 判斷用戶端是否已斷線的變數
        bool isClientConnected = true;

        int counter = 1;

        foreach (var item in data)
        {
            // 模擬每筆資料的處理時間
            await Task.Delay(50);

            // 假設我們每 1,000 筆檢查一次用戶端狀態
            if (counter % 1000 == 0)
            {
                // 我們需要靠 sc (同步內容) 取得原本的執行緒內容，並藉此還原 HttpContext.Current 物件
                // 以我假設的情境來說，必須用 Send 同步的方式執行 SendOrPostCallback，執行完才能繼續
                sc.Send(state => {
                    isClientConnected = System.Web.HttpContext.Current.Response.IsClientConnected;
                }, null);

                if (!isClientConnected)
                {
                    // 注意：撰寫 Log 的時候，有時候會需要取得部分 HttpContext 資訊記錄起來
                    // 但我們因為還在背景執行緒中，因此不在 sc 裡面的時候依然無法取得 HttpContext 相關資訊
                    log.Information("Client Disconnected");

                    // 用戶端斷線就不繼續處理資料
                    break;
                }
            }

            counter++;
        }

        // 回傳處理過的資料
        return data;
    });

    return result;
}
```

重點說明：

1.  上述程式的優點，就在於我們可以每處理 1,000 筆資料就檢查一次用戶端是否斷線，但你必須透過 `SynchronizationContext` 才有辦法取用到 `System.Web.HttpContext.Current` 的值。
    
2.  由於透過 `SynchronizationContext` 取用**執行緒**的時候，必須確保**原始的執行緒**沒有人在用，否則可能會引發 **Deadlock** (死結) 或執行效率低落的結果。
    
3.  由於 `SynchronizationContext` 有 [Post()](https://learn.microsoft.com/zh-tw/dotnet/api/system.threading.synchronizationcontext.post?view=net-5.0&WT.mc_id=DT-MVP-4015686) 與 [Send()](https://learn.microsoft.com/zh-tw/dotnet/api/system.threading.synchronizationcontext.send?view=net-5.0&WT.mc_id=DT-MVP-4015686) 這兩個 API，其中 `Post` 是「非同步」的版本，而 `Send` 是「同步」的版本。
    
    同步的 `Send` API 可能比較好理解，他會先取得 `SynchronizationContext` 中的執行緒資訊，執行完 `SendOrPostCallback` 委派之後，才會往下執行。意思也就是說，當 `SynchronizationContext` 中的執行緒沒空的時候，這段程式就會卡住一段時間，必須等到 Thread 有空才會執行下去。
    
    非同步的 `Post` API 其實是將 `SendOrPostCallback` 委派加入到**工作排程器**(TaskScheduler)中，這個 `SendOrPostCallback` 委派並不會立刻執行，而是等到 `SynchronizationContext` 中的執行緒有空的時候才會執行。意思也就是說，即便 `SynchronizationContext` 中的執行緒沒空的時候，這段 `sc.Post()` 方法並不會卡住，程式會繼續執行下去。因此，你有很大的機率會遇到送出多個 `SendOrPostCallback` 之後都沒有立即執行，而在執行好幾次 `sc.Post()` 方法後，傳入的 `SendOrPostCallback` 才突然執行好幾次的狀況。
    
    以我們上述 ASP.NET 的例子來說，我們需要即時判斷用戶端是否斷線，才能繼續往下走，所以用「同步」的 `Send` API 才比較符合真實的情境要求。
    

### 相關連結

-   [Await, SynchronizationContext, and Console Apps](https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/?WT.mc_id=DT-MVP-4015686)
-   [Async and Await: Here be dragons](https://gist.github.com/ghuntley/e5b5642ecc4428255e61185bb79856e4)
-   [Learning SynchronizationContext, async, and await](https://www.infoworld.com/article/2960463/my-two-cents-on-synchronizationcontext-async-and-await.html)
-   [Parallel Computing - It's All About the SynchronizationContext](https://learn.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext?WT.mc_id=DT-MVP-4015686)
-   [Understanding SynchronizationContext: Part I](https://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I)
-   [Understanding SynchronizationContext: Part II](https://www.codeproject.com/Articles/32113/Understanding-SynchronizationContext-Part-II)
-   [.NET 程式鎖死與 SynchronizationContext](https://www.huanlintalk.com/2016/01/asyc-deadlock-in-aspbet.html)
-   [有關 Task ConfigureAwait() 的一些事情](https://dotblogs.com.tw/aspnetshare/2018/06/03/configureawaiter)
