在 TransactionScope 中触发并忘记异步代码的最佳方法

Best way to fire and forget async code inside TransactionScope

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

所以最初我做了一些类似于我在下面的代码中评论的 methodFails 的东西。它给了我一个很好的 "System.InvalidOperationException: 'TransactionScope nested incorrectly'"。我在 SO 中查找了有类似问题的人,并发现了这个 Question,其中 ZunTzu 的回答给了我使用 TransactionScopeAsyncFlowOption.Enabled 选项 method1 的想法,这正如我对 methodFails 但无一例外。

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

所以我的问题是:哪种方法更好,method1method2,或者我没有考虑过的第三种方法?我倾向于 method1,因为它听起来像 "the people making the TransactionScope class put that TransactionScopeAsyncFlowOption there for a reason"。但是 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!!");
        }
    }

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

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

QueueBackgroundWorkItem总结如下:

The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed.

But the fact that TransactionScopeAsyncFlowOption.Enabled is not the default for a TransactionScope makes me think that maybe there is a performance hit by enabling that, and fire-and-forget may be a special case where I can save that performance hit.

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

this article中:

You might not know this, but the 4.5.0 version of the .NET Framework contains a serious bug regarding System.Transactions.TransactionScope and how it behaves with async/await. Because of this bug, a TransactionScope can't flow through into your asynchronous continuations. This potentially changes the threading context of the transaction, causing exceptions to be thrown when the transaction scope is disposed.

This is a big problem, as it makes writing asynchronous code involving transactions extremely error-prone.

The good news is that as part of the .NET Framework 4.5.1, Microsoft released the fix for that "asynchronous continuation" bug. The thing is that developers like us now need to explicitly opt-in to get this new behavior. Let's take a look at how to do just that.

  • A TransactionScope wrapping asynchronous code needs to specify TransactionScopeAsyncFlowOption.Enabled in its constructor.