使用异步/等待使TransactionScope正常工作

130

我正在尝试将async/await集成到我们的服务总线中。

我根据这个示例http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx实现了一个基于SingleThreadSynchronizationContext的同步上下文。

它完全正常运行,除了一件事:TransactionScope。我在TransactionScope中等待任务完成,但它却破坏了TransactionScope

TransactionScope似乎无法与async/await相兼容,可能是因为它使用ThreadStaticAttribute将数据存储在线程中。我收到以下异常:

"TransactionScope嵌套错误。"

我尝试在排队任务之前保存TransactionScope数据并在运行任务之前还原它,但似乎没有任何改变。而且TransactionScope代码很混乱,所以很难理解其中的情况。

有没有办法使其正常工作?是否有替代TransactionScope的方法?


这里有一个非常简单的代码来重现TransactionScope错误http://pastebin.com/Eh1dxG4a,除了这里的异常是Transaction Aborted。 - Yann
你能不能只使用普通的SQL事务?还是你正在跨多个资源? - Marc Gravell
我正在跨越多个资源。 - Yann
看起来你需要将作用域传递到异步方法中,或者提供一种从某种通用上下文中检索它的方式,该上下文与你的工作单元有关。 - Bertrand Le Roy
为每个顶级的TransactionScope,您需要一个拥有自己的 SingleThreadSynchronizationContext 的独立线程。 - Stephen Cleary
我试图在同步上下文的Post方法中保存ThreadStatic作用域上下文。但是该方法在与发布的方法相同的线程上执行。 所以现在我在声明TransactionScope后立即保存上下文,这样就可以了。 当然,我正在使用反射访问作用域ContextData,这是非常错误的。但至少它能工作! - Yann
4个回答

186
在.NET Framework 4.5.1中,有一组为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();
}

15

回答有点晚,但我遇到了和MVC4一样的问题,我通过右键点击项目并选择“属性”将我的项目从4.5更新到4.5.1。选择应用程序选项卡,将目标框架更改为4.5.1,并按照以下方式使用事务。

using (AccountServiceClient client = new AccountServiceClient())
using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
}

5
这与被接受的答案有什么不同? - Liam
@Liam 没有被接受的答案。 - David Klempfner
@Liam,目前还没有被接受的答案。 - undefined

6
你可以使用 Transaction.DependentClone() 方法创建 DependentTransaction
static void Main(string[] args)
{
  // ...

  for (int i = 0; i < 10; i++)
  {

    var dtx = Transaction.Current.DependentClone(
        DependentCloneOption.BlockCommitUntilComplete);

    tasks[i] = TestStuff(dtx);
  }

  //...
}


static async Task TestStuff(DependentTransaction dtx)
{
    using (var ts = new TransactionScope(dtx))
    {
        // do transactional stuff

        ts.Complete();
    }
    dtx.Complete();
}

使用 DependentTransaction 管理并发

http://adamprescott.net/2012/10/04/transactionscope-in-multi-threaded-applications/


2
Adam Prescott的示例子任务没有标记为async。如果你将"do transactional stuff"替换为类似await Task.Delay(500)的内容,这种模式也会因为"TransactionScope nested incorrectly"而失败,因为外层的TransactionScope(上面的示例中未显示)在子任务正确完成之前就退出了作用域。将await替换为Task.Wait()可以解决问题,但是这样你就失去了使用async的好处。 - mdisibio
这是一个更困难的解决问题的方式。TransactionScope是为了隐藏所有的管道。 - Eniola

1

如果还有人关注这个帖子...

Microsoft.Data.SqlClient v3.0.1 似乎修复了.NET Framework中异步/等待函数与System.Transactions死锁问题!我正在使用4.8。我使用显式分布式事务方法,通过使用多个连接允许对单个SQL服务器进行并行查询执行,但我确信使用TransactionScope的行为也更好。

创建一个CommittableTransaction,创建任意数量的SqlConnection,它们登记在从父Committable创建的DependentTransaction中,在连接上并行执行查询,在查询执行后完成依赖项,然后提交/回滚父Committable。

我用3个并行异步插入测试了它们到同一数据库,使用3个不同的连接。当父Committable正在进行时,我使用消息框提示提交或回滚。在分布式事务处于活动状态时,表被锁定。我无法从ssms中选择它们。选择提交或回滚后,都按预期工作。

上周使用Microsoft.Data.SqlClient 3.0.0是不可能的,因为.Commit()方法会在异步函数中死锁。我甚至尝试了BeginCommit/EndCommit和各种方法。甚至存在在异步函数中登记从属者的问题,但现在也已经修复了。现在最简单的显式分布式事务方法可以与异步/等待一起使用。


可能我过早地说了关于登记依赖项已经修复的话。今天进行了大量测试后,SqlConnection.EnlistTransaction()似乎会随机引发异常。一旦出现异常,您将看到有关数据读取器仍处于打开状态并需要关闭(我在这些测试中根本没有使用它们)或无法登记因为每次连续运行都处于错误状态的事务的消息。有时等待几分钟会允许成功尝试,有时我必须在它再次工作之前重置DTC服务。至少没有死锁。 - Don Reynolds
我的怀疑是当您异步打开并登记一堆sql连接时,enlist有时会失败,因为DTC无法正确推进事务或在一个或多个连接开始执行SQL代码后无法将新连接添加到事务中。如果我在每次调用EnlistTransaction()之后放置人为的2秒暂停,以使所有同时创建的连接都有机会在执行查询之前注册,那么相同的代码将起作用。 - Don Reynolds

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接