【问题标题】:How to dispose TransactionScope in cancelable async/await?如何在可取消的异步/等待中处理 TransactionScope?
【发布时间】:2012-10-04 09:44:21
【问题描述】:

我正在尝试使用新的 async/await 功能来异步处理数据库。由于某些请求可能很长,我希望能够取消它们。我遇到的问题是TransactionScope 显然具有线程亲和性,并且似乎在取消任务时,它的Dispose() 在错误的线程上运行。

具体来说,当调用.TestTx() 时,我在task.Wait () 上得到以下AggregateException 包含InvalidOperationException

"A TransactionScope must be disposed on the same thread that it was created."

代码如下:

public void TestTx () {
    var cancellation = new CancellationTokenSource ();
    var task = TestTxAsync ( cancellation.Token );
    cancellation.Cancel ();
    task.Wait ();
}

private async Task TestTxAsync ( CancellationToken cancellationToken ) {
    using ( var scope = new TransactionScope () ) {
        using ( var connection = new SqlConnection ( m_ConnectionString ) ) {
            await connection.OpenAsync ( cancellationToken );
            //using ( var command = new SqlCommand ( ... , connection ) ) {
            //  await command.ExecuteReaderAsync ();
            //  ...
            //}
        }
    }
}

更新:被注释掉的部分是为了表明一旦连接打开,就有一些事情要做——异步地——但是重现问题不需要该代码。

【问题讨论】:

    标签: c# .net database transactions async-await


    【解决方案1】:

    在 .NET Framework 4.5.1 中,有一组 new constructors for TransactionScope 采用 TransactionScopeAsyncFlowOption 参数。

    根据 MSDN,它支持跨线程延续的事务流。

    我的理解是,它是为了让你可以这样写代码:

    // transaction scope
    using (var scope = new TransactionScope(... ,
      TransactionScopeAsyncFlowOption.Enabled))
    {
      // connection
      using (var connection = new SqlConnection(_connectionString))
      {
        // open connection asynchronously
        await connection.OpenAsync();
    
        using (var command = connection.CreateCommand())
        {
          command.CommandText = ...;
    
          // run command asynchronously
          using (var dataReader = await command.ExecuteReaderAsync())
          {
            while (dataReader.Read())
            {
              ...
            }
          }
        }
      }
      scope.Complete();
    }
    

    我还没试过,不知道能不能用。

    【讨论】:

    • 如果无法升级到 4.5.1 怎么办?那有什么办法呢?
    • @JobaDiniz 您仍然可以通过调用 DbConnection.BeginTransaction 来使用老式(非环境)ADO.NET 事务而不是 TransactionScope。
    【解决方案2】:

    我知道这是一个旧线程,但如果有人遇到 System.InvalidOperationException 问题:必须在创建它的同一线程上处理 TransactionScope。

    解决方案是至少升级到 .net 4.5.1 并使用如下事务:

    using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
    {
       //Run some code here, like calling an async method
       await someAsnycMethod();
       transaction.Complete();
    } 
    

    现在事务在方法之间共享。看看下面的链接。它提供了一个简单的例子和​​更多的细节

    如需了解完整详情,请查看This

    【讨论】:

    • 你拯救了我的一天,谢谢
    【解决方案3】:

    问题源于我在控制台应用程序中对代码进行原型设计,我没有在问题中反映出来。

    async/await 在await 之后继续执行代码的方式取决于SynchronizationContext.Current 的存在,而控制台应用程序默认没有,这意味着继续使用当前的TaskScheduler 执行,这是一个ThreadPool,所以它(可能?)在不同的线程上执行。

    因此,只需要一个SynchronizationContext 来确保TransactionScope 被部署在它创建的同一线程上。 WinForms 和 WPF 应用程序将默认拥有它,而控制台应用程序可以使用自定义的,也可以从 WPF 借用DispatcherSynchronizationContext

    这里有两篇详细解释机制的精彩博文:
    Await, SynchronizationContext, and Console Apps
    Await, SynchronizationContext, and Console Apps: Part 2

    【讨论】:

      【解决方案4】:

      针对 .NET Framework 4.6+、.NET Core 2.1+ 或 .NET Standard 2.0+

      考虑使用Microsoft.Data.SqlClient,它将 .NET Framework 和 .NET Core 的 System.Data.SqlClient 组件集中在一个屋檐下。 如果您想使用一些较新的 SQL Server 功能,也很有用。

      查看repo 或从nuget 提取。

      添加包后添加using语句:

      using Microsoft.Data.SqlClient;
      

      使用 C# 8 的示例:

      // transaction scope
      using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
      
      // connection
      await using var connection = new SqlConnection(_connectionString);
      
      // open connection asynchronously
      await connection.OpenAsync();
      await using var command = connection.CreateCommand();
      command.CommandText = "SELECT CategoryID, CategoryName FROM Categories;";
      
      // run command asynchronously
      await using var dataReader = await command.ExecuteReaderAsync();
      
      while (dataReader.Read())
      {
          Console.WriteLine("{0}\t{1}", dataReader.GetInt32(0), dataReader.GetString(1));
      }
      
      scope.Complete();
      

      【讨论】:

        【解决方案5】:

        是的,您必须将事务范围保持在单个线程上。由于您在异步操作之前创建事务范围,并在异步操作中使用它,因此事务范围不在单个线程中使用。 TransactionScope 不是为那样使用而设计的。

        我认为一个简单的解决方案是将 TransactionScope 对象和 Connection 对象的创建移到异步操作中。

        更新

        由于异步操作在 SqlConnection 对象中,我们无法更改它。 我们能做的是enlist the connection in the transaction scope。我会以异步方式创建连接对象,然后创建事务范围,并登记事务。

        SqlConnection connection = null;
        // TODO: Get the connection object in an async fashion
        using (var scope = new TransactionScope()) {
            connection.EnlistTransaction(Transaction.Current);
            // ...
            // Do something with the connection/transaction.
            // Do not use async since the transactionscope cannot be used/disposed outside the 
            // thread where it was created.
            // ...
        }
        

        【讨论】:

        • 您能否详细说明您的第二点?将创作移到哪里?
        • 您是否建议只异步打开连接,并为实际工作负载使用阻塞调用?还是我又错过了什么?
        • 目前在您的问题中,您只能异步获取连接。我已经按照您的示例进行操作,但是如果您使用一些实际工作量来更新您的问题,那么我也可以更新我的答案。
        • 问题中的代码是我能想出的用于演示该问题的最小示例。我认为这将是明确的打开然后关闭连接不是目标。我现在更新了代码,以表明连接打开后存在一些异步工作负载。
        猜你喜欢
        • 1970-01-01
        • 2020-04-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-02
        • 2020-09-13
        • 1970-01-01
        • 2021-11-01
        相关资源
        最近更新 更多