【问题标题】:Best way to fire and forget async code inside TransactionScope在 TransactionScope 中触发和忘记异步代码的最佳方法
【发布时间】:2018-02-14 07:59:10
【问题描述】:

我正在为 TransactionScope 对象的 using 块内做一些事情。在某些时候,我想通过触发并忘记来调用一些异步代码(我不想等待结果,而且我对该调用期间发生的事情不感兴趣)并且我希望该代码不属于交易(通过使用TransactionScopeOption.Suppress 选项)。

所以最初我做了一些类似于我在下面的代码中评论过的methodFails。它给了我一个很好的“System.InvalidOperationException:'TransactionScope 嵌套不正确'”。我在 SO 中查找了有类似问题的人,并发现了这个 Question,其中 ZunTzu 的答案给了我使用 TransactionScopeAsyncFlowOption.Enabled 选项的 method1 的想法,这与我对 methodFails 的预期一样有效,但没有例外。

然后我想到了一个替代方案,我将method2 放入其中,将异步代码放入第三种方法(method3)中,该方法由触发后调用调用,而TransactionScopeOption.Suppress 选项保留在非-异步method2。在我的示例程序中,这种方法似乎与method1 一样有效。

所以我的问题是:哪种方法更好,method1method2,或者可能是我没想到的第三种方法?我倾向于method1,因为这听起来像是“制作 TransactionScope 类的人将 TransactionScopeAsyncFlowOption 放在那里是有原因的”。但是TransactionScopeAsyncFlowOption.Enabled 不是 TransactionScope 的默认值这一事实让我认为启用它可能会影响性能,而即发即弃可能是我可以保存该性能影响的一种特殊情况。

示例代码:

    class Program
    {
        static void Main(string[] args)
        {
            using (TransactionScope scope1 = new TransactionScope())
            {
                // Do some stuff in scope1...

                // Start calls that could execute async code
                //Task a = methodFails(); // This commented method would launch exception: System.InvalidOperationException: 'TransactionScope nested incorrectly'
                Task b = method1(); // Fire and forget
                method2();

                // Rest of stuff in scope1 ...
            }
            Console.ReadLine();
        }

        static async Task methodFails()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 0.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 0.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 0.3!!");
        }

        static async Task method1()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 1.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 1.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 1.3!!");
        }

        static void method2()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Task ignored = method3(); // Fire and forget
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 2.2!!");
        }

        static async Task method3()
        {
            //Do non-transactional work here
            Console.WriteLine("Hello World 2.1!!");
            await Task.Delay(10000);
            Console.WriteLine("Hello World 2.3!!");
        }
    }

【问题讨论】:

  • 如果您只是想在事务范围之外的即发即弃中运行您的代码,我会说这是 Task.Run 的一个很好的用例
  • @Kevin Gosse 我前段时间在 Stephen Cleary 的博客中读到 Task.Run 应该用于 CPU 绑定代码。在我的生产代码中,“Task.Delay”调用实际上是一个存储过程调用,需要几秒钟才能完成,所以我认为它更适合 async-await 场景
  • 这种评论有时让我想知道那篇博客文章是否带来的伤害大于帮助。当您想将工作卸载到另一个线程时,Task.Run 很有用。这对于 I/O 绑定操作几乎没有意义,而且这是一个容易犯的错误,这就是 Stephen 做出近似“Task.Run = CPU bound”的原因。但这并不意味着你不应该在它帮助你的时候使用它,只要你了解它的作用
  • 我对那篇文章的解释是“如果有一种(合理的)方法可以使用异步等待完成 I/O 工作,那就去吧”。就我而言,我最初发布的两个选项都不是解决问题的坏方法,并且 async-await 语法比 Task.Run 传达了正在完成的工作的性质。但我同意你的观点,值得评估所有可用的工具。

标签: c# .net async-await transactionscope fire-and-forget


【解决方案1】:

但是 TransactionScopeAsyncFlowOption.Enabled 不是 TransactionScope 的默认值这一事实让我认为启用它可能会影响性能,而即发即弃可能是我可以保存该性能影响的特殊情况。

TransactionScopeAsyncFlowOption.Enabled 是为了向后兼容的目的而引入的,当时他们修复了一个错误。奇怪的是,除非您通过设置此标志“选择加入”,否则您不会从错误修复中受益。他们这样做是为了使错误修复不会破坏任何依赖错误行为的现有代码。

this article:

您可能不知道这一点,但 .NET Framework 的 4.5.0 版本包含一个关于 System.Transactions.TransactionScope 及其与 async/await 的行为方式的严重错误。由于此错误,TransactionScope 无法流入您的异步延续。这可能会更改事务的线程上下文,导致在释放事务范围时引发异常。

这是一个大问题,因为它使得编写涉及事务的异步代码非常容易出错。

好消息是,作为 .NET Framework 4.5.1 的一部分,Microsoft 发布了针对“异步延续”错误的修复程序。问题是像我们这样的开发人员现在需要明确选择加入才能获得这种新行为。让我们看看如何做到这一点。

  • 包装异步代码的 TransactionScope 需要在其构造函数中指定 TransactionScopeAsyncFlowOption.Enabled。

【讨论】:

  • 有趣的文章,特别是阐明为什么不是默认值的部分。我仍然怀疑method2 不是异步的,尽管它调用了一个异步方法,所以也许我没有“包装异步”代码。无论如何,我想我会避免过早的优化,甚至可能不是优化,并选择“启用”选项。谢谢!
【解决方案2】:

您可以在 HostingEnvironment.QueueBackgroundWorkItem 调用中调用您的异步方法。

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
    await LongRunningMethodAsync();
});

QueueBackgroundWorkItem总结如下:

HostingEnvironment.QueueBackgroundWorkItem 方法让您 安排小的背景工作项目。 ASP.NET 跟踪这些项目并 防止 IIS 突然终止工作进程,直到所有 后台工作项已完成。

【讨论】:

  • 不知道那个 QueueBackgroundWorkItem,听起来不错,但是......不幸的是,对于这个特定问题,我不在 ASP.NET 环境中。
猜你喜欢
  • 2013-09-01
  • 1970-01-01
  • 2010-12-31
  • 1970-01-01
  • 2014-12-06
  • 2018-07-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多