在 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 運作流程
- 進入 AddOrder 依序執行至 SaveOrder2DbAsync 非同步作業
- 進入 SaveOrder2DbAsync 非同步方法
- 依序執行所有代碼直至遇見 await 關鍵字
- 遇到 await 首先會把執行續主導權交回 SaveOrder2DbAsync 呼叫端 (AddOrder)
- [ 同時產生非同步支線 ]
- 藍1. 等待 job task 結束
- 藍2. 當 task completed 後取得回傳值,並接續執行 await 後面的代碼
- 藍3. 完成 SaveOrder2DbAsync 作業就回到呼叫端 (AddOrder) 中的 await 點
- 接續處理呼叫 SaveOrder2DbAsync 後面的代碼
- 遇到 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 這類非同步作業,如此就可以在非同步情況下完成交易行為。
執行後不再拋出「TransactionScope 必須在當初建立所在的執行緒上進行配置」錯誤,可以順利完成作業。
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !