如何在可取消的async / await中部署TransactionScope?

时间:2012-10-04 09:44:21

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

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

具体来说,在致电.TestTx()时,我会在AggregateException上收到包含InvalidOperationException的以下task.Wait ()

"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 ();
            //  ...
            //}
        }
    }
}

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

5 个答案:

答案 0 :(得分:95)

在.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();
}

我还没有尝试过,所以我不知道它是否会起作用。

答案 1 :(得分:4)

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

await依赖于SynchronizationContext.Current之后,async / await继续执行代码的方式,并且控制台应用程序默认没有,这意味着继续执行使用当前TaskScheduler,这是ThreadPool,因此它(可能?)在不同的线程上执行。

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

以下是两篇很好的博客文章,详细解释了这些机制:
Await, SynchronizationContext, and Console Apps
Await, SynchronizationContext, and Console Apps: Part 2

答案 2 :(得分:1)

我知道这是一个旧线程,但是如果有人遇到了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 :(得分:0)

是的,你必须在一个线程上保留transactioncope。由于您在异步操作之前创建了transactioncope,并在异步操作中使用它,因此不会在单个线程中使用transactionscope。 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.
    // ...
}

答案 4 :(得分:0)

用于定位.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();