[C#] 排除TransactionScope 中使用await 所产生的跨执行续交易错误

来源:转载



在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 AddOrder()
{
var result = string.Empty;
// 使用交易確保異動完整
using (TransactionScope ntx = new TransactionScope())
{
// 呼叫執行非同步作業 (將資料寫入DB)
var job = SaveOrder2DbAsync();
// 處理其它需在交易內完成的變動
// ....
// 等待非同步作業完成
result = await job;
}
return Ok(result);
}
private async Task 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 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 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 必须在当初建立所在的执行绪上进行配置」错误,可以顺利完成作业。

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


若内容有误或有其他建议请不吝留言给笔者喔 !
此内容翻译自:dotblogs.com.tw 搞搞就懂





分享给朋友:
您可能感兴趣的文章:
随机阅读: