Подтвердить что ты не робот

Как утилизировать TransactionScope в отмене async/wait?

Я пытаюсь использовать новую функцию 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 ();
            //  ...
            //}
        }
    }
}

ОБНОВЛЕНО: закомментированная часть - показать, что что-то будет сделано - асинхронно - с соединением после его открытия, но этот код не требуется для воспроизведения проблемы.

4b9b3361

Ответ 1

Проблема связана с тем, что я прототипировал код в консольном приложении, о котором я не думал в вопросе.

Способ async/await продолжает выполнять код после await зависит от наличия SynchronizationContext.Current, а консольное приложение по умолчанию не имеет его, что означает, что продолжение выполняется с использованием текущего TaskScheduler, который является ThreadPool, поэтому он (потенциально?) выполняется в другом потоке.

Таким образом, просто нужно иметь SynchronizationContext, который обеспечит, чтобы TransactionScope располагался в том же потоке, который был создан. WinForms и WPF-приложения будут иметь его по умолчанию, тогда как консольные приложения могут использовать пользовательский или заимствовать DispatcherSynchronizationContext из WPF.

Вот два замечательных сообщения в блоге, которые подробно объясняют механику:
Ожидание, синхронизацияContext и консольные приложения
Ожидание, SynchronizationContext и консольные приложения: Часть 2

Ответ 2

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

Я еще не пробовал, поэтому не знаю, будет ли это работать.

Ответ 3

Да, вы должны держать вас в операциях на одном потоке. Поскольку вы создаете транзакцию перед асинхронным действием и используете ее в действии async, транзакция не используется в одном потоке. 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.
    // ...
}

Ответ 4

Я знаю, что это старый поток, но если кто-то столкнулся с проблемой 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();
} 

Теперь транзакция распределяется между методами. Посмотрите на ссылку ниже. Это простой пример и более подробно

Для получения полной информации, посмотрите на это