web-dev-qa-db-ja.com

TransactionScopeをキャンセル可能な非同期/待機で破棄する方法は?

新しい非同期/待機機能を使用して、非同期でDBを操作しようとしています。一部のリクエストは長くなる可能性があるため、キャンセルできるようにしたいと考えています。私が遭遇している問題は、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 ();
            //  ...
            //}
        }
    }
}

更新:コメントアウトされた部分は、接続が開かれると、非同期で実行する必要があることを示していますが、問題を再現するためにそのコードは必要ありません。

60
chase

問題は、コンソールアプリケーションでコードのプロトタイプを作成していたという事実に起因しますが、質問には反映されていません。

Async/awaitがawaitの後にコードを実行し続ける方法は、SynchronizationContext.Currentの存在に依存し、コンソールアプリケーションにはデフォルトで1つありません。つまり、現在のTaskScheduler、つまりThreadPoolを使用して継続が実行されます。なので、(potentially?)は別のスレッドで実行されます。

したがって、SynchronizationContextが作成された同じスレッドに確実に配置されるTransactionScopeが必要です。 WinFormsとWPFアプリケーションはデフォルトでそれを持っていますが、コンソールアプリケーションはカスタムアプリケーションを使用するか、WPFからDispatcherSynchronizationContextを借りることができます。

メカニズムを詳細に説明する2つの優れたブログ投稿を次に示します。
Await、SynchronizationContext、およびコンソールアプリ
Await、SynchronizationContext、およびコンソールアプリ:パート2

4
chase

.NET Framework 4.5.1には、TransactionScopeAsyncFlowOptionパラメーターを取る一連の TransactionScopeの新しいコンストラクター があります。

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

まだ試していないので、うまくいくかわかりません。

98
ZunTzu

私はこれが古いスレッドであることを知っていますが、誰かが問題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 をご覧ください。

2
Terry Slack

はい、transactionscopeを1つのスレッドで維持する必要があります。非同期アクションの前にトランザクションスコープを作成していて、それを非同期アクションで使用しているため、トランザクションスコープは単一のスレッドでは使用されません。 TransactionScopeは、そのように使用するように設計されていません。

TransactionScopeオブジェクトとConnectionオブジェクトの作成を非同期アクションに移動することが、私が考える簡単な解決策でしょう。

更新

非同期アクションはSqlConnectionオブジェクト内にあるため、変更できません。私たちにできることは 接続をトランザクションスコープに入れる です。接続オブジェクトを非同期で作成してから、トランザクションスコープを作成し、トランザクションを参加させます。

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.
    // ...
}
0
Maarten