新しい非同期/待機機能を使用して、非同期で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 ();
// ...
//}
}
}
}
更新:コメントアウトされた部分は、接続が開かれると、非同期で実行する必要があることを示していますが、問題を再現するためにそのコードは必要ありません。
問題は、コンソールアプリケーションでコードのプロトタイプを作成していたという事実に起因しますが、質問には反映されていません。
Async/awaitがawait
の後にコードを実行し続ける方法は、SynchronizationContext.Current
の存在に依存し、コンソールアプリケーションにはデフォルトで1つありません。つまり、現在のTaskScheduler
、つまりThreadPool
を使用して継続が実行されます。なので、(potentially?)は別のスレッドで実行されます。
したがって、SynchronizationContext
が作成された同じスレッドに確実に配置されるTransactionScope
が必要です。 WinFormsとWPFアプリケーションはデフォルトでそれを持っていますが、コンソールアプリケーションはカスタムアプリケーションを使用するか、WPFからDispatcherSynchronizationContext
を借りることができます。
メカニズムを詳細に説明する2つの優れたブログ投稿を次に示します。
Await、SynchronizationContext、およびコンソールアプリ
Await、SynchronizationContext、およびコンソールアプリ:パート2
.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();
}
まだ試していないので、うまくいくかわかりません。
私はこれが古いスレッドであることを知っていますが、誰かが問題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 をご覧ください。
はい、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.
// ...
}