1. ホーム
  2. c#

キャンセル可能なasync/awaitでTransactionScopeを廃棄する方法は?

2023-07-10 15:16:35

質問

新しいasync/await機能を使って、DBで非同期処理を行おうとしています。いくつかのリクエストは長くなることがあるので、それらをキャンセルできるようにしたいです。私が遭遇している問題は TransactionScope がスレッドアフィニティを持っているようで、タスクをキャンセルするときに、その Dispose() は間違ったスレッドで実行されてしまうようです。

具体的には .TestTx() を呼び出すと、次のようになります。 AggregateException を含む InvalidOperationExceptiontask.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 ();
            //  ...
            //}
        }
    }
}

UPDATED: コメントアウトされた部分は、一度オープンした接続に対して非同期に行われる何かがあることを示すためのものです。

どのように解決するのですか?

問題は、私がコンソールアプリケーションでコードをプロトタイプ化していたことに起因しており、質問には反映されていませんでした。

async/awaitがコードの実行を継続する方法は await が存在するかどうかに依存します。 SynchronizationContext.Current があるかどうかに依存し、コンソールアプリケーションには デフォルトでそれが無いため、継続は現在の TaskScheduler であり、これは ThreadPool であるため、それ( は潜在的に? )は別のスレッドで実行されます。

したがって、単純に SynchronizationContext を確実にする TransactionScope が作成されたのと同じスレッドで破棄されるようにします。WinForms と WPF アプリケーションはデフォルトでこれを備えていますが、コンソールアプリケーションではカスタムのものを使用するか、あるいは DispatcherSynchronizationContext を WPF から借りることができます。

この仕組みを詳しく説明している素晴らしいブログ記事を2つ紹介します。

Await、SynchronizationContext、およびコンソールアプリ

Await、SynchronizationContext、Console Apps。パート2