[C#] 排除 TransactionScope 中使用 await 所產生的跨執行續交易錯誤

  • 3961
  • 0
  • C#
  • 2017-10-12

在 TransactionScope 中使用 await 會發生 「TransactionScope 必須在當初建立所在的執行緒上進行配置」錯誤訊息,本文以此錯誤來檢視 async / await 機制與流程,並且透過設定來排除 Task 或 async/await 這類非同步作業所產生的跨執行續交易問題。

起因


最近幫忙調整 Web API 為非同步作業來提升網站效能時,意外地造成 「TransactionScope 必須在當初建立所在的執行緒上進行配置」錯誤發生,察看後發現在 TranscationScope 中有 await 非同步方法,所以沒意外的話兇手就是它了。

 

 

理解 Async / Await 運作機制


由此錯誤訊息可得知在 TransactionScope 內的任何操作必須保持與建立 TransactionScope 當下的執行續相同,而錯誤發生原因可推論在 await 後接續執行的執行續有機會與原本的不同;為加深印象觀念及實踐「搞搞就懂」的精神,筆者使用一個簡單的範例來進行說明,如果不想了解細節的朋友可直接略過,往下一章節取得解決方式囉!

以 Web API -  AddOrder 為例,在 TransactionScope 中呼叫 SaveOrder2DbAsync 非同步方法,並使用 await 來等待非同步作業結束,以此模擬資料寫入至 DB 中的非同步動作。完成程式碼如下。 


public class TranScopeController : ApiController
{
    [HttpGet]
    public async Task<IHttpActionResult> AddOrder()
    {
        var result = string.Empty;

        // 使用交易確保異動完整
        using (TransactionScope ntx = new TransactionScope())
        {
            // 呼叫執行非同步作業 (將資料寫入DB)
            var job = SaveOrder2DbAsync();
            
            // 處理其它需在交易內完成的變動
            // ....

            // 等待非同步作業完成
            result = await job;
        }

        return Ok(result);
    }

    private async Task<string> SaveOrder2DbAsync()
    {
        var result = string.Empty;

        // 模擬對DB進行非同步操作
        var job = Task.Run(async () =>
        {
            await Task.Delay(1000);
            return "it's done!!";
        });

        // 等待非同步作業完成
        result = await job;

        return result;
    }
}

 

先來剖析一下 async / await 運作流程

  1. 進入 AddOrder 依序執行至 SaveOrder2DbAsync 非同步作業
  2. 進入 SaveOrder2DbAsync 非同步方法
  3. 依序執行所有代碼直至遇見 await 關鍵字
  4. 遇到 await 首先會把執行續主導權交回 SaveOrder2DbAsync 呼叫端 (AddOrder)
    • [ 同時產生非同步支線 ]
    • 藍1. 等待 job task 結束 
    • 藍2. 當 task completed 後取得回傳值,並接續執行 await 後面的代碼
    • 藍3. 完成 SaveOrder2DbAsync 作業就回到呼叫端 (AddOrder) 中的 await 點
  5. 接續處理呼叫 SaveOrder2DbAsync 後面的代碼
  6. 遇到 await 首先會把執行續主導權交回 AddOrder 呼叫端
    • [ 同時產生非同步支線 ]
    • 綠1. 等待 job task 結束 
    • 綠2. 當 task completed 後取得回傳值,並接續執行 await 後面的代碼
    • 綠3. 完成 AddOrder 作業就回到呼叫端中的 await 點

 

動手記錄各階段 thread Id 及 synchronization context 來驗證一下吧!

public class TranScopeController : ApiController
{
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

    [HttpGet]
    public async Task<IHttpActionResult> AddOrder()
    {
        var result = string.Empty;
        using (TransactionScope ntx = new TransactionScope())
        {
            logger.Info($"[01] {SynchronizationContext.Current}");
            var job = SaveOrder2DbAsync();
            logger.Info($"[04] {SynchronizationContext.Current}");
            result = await job;
            logger.Info($"[06] {SynchronizationContext.Current}");
        }
        return Ok(result);
    }

    private async Task<string> SaveOrder2DbAsync()
    {
        var result = string.Empty;
        logger.Info($"[02] {SynchronizationContext.Current}");
        var job = Task.Run(async () =>
        {
            await Task.Delay(1000);
            return "it's done!!";
        });
        logger.Info($"[03] {SynchronizationContext.Current}");
        result = await job;
        logger.Info($"[05] {SynchronizationContext.Current}");
        return result;
    }
}

 

筆者共在 TransactionScope 中記錄了 01 04 06 三個記錄點,結果顯示 06 所使用的執行續與 01 04 不相同 , 由此驗證 await 後面接續執行動作的 thread 可能會與原本建立 TransactionScope 的不同,確實有機會造成錯誤發生。

 

 

排除錯誤


在 .net framework 4.5.1 後提供了 TransactionScopeAsyncFlowOption 列舉型別,可以在 TransactionScope 建構式中直接設定啟用跨執行續處理機制來因應 Task 或 async/await 這類非同步作業,如此就可以在非同步情況下完成交易行為。

TransactionScopeAsyncFlowOption.Enabled

 

執行後不再拋出「TransactionScope 必須在當初建立所在的執行緒上進行配置」錯誤,可以順利完成作業。

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !