it-swarm.com.ru

Как разместить TransactionScope в отменяемой асинхронной/ожидающей?

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

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

46
chase

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

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

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

Вот два отличных поста в блоге, которые подробно объясняют механику:
Await, SynchronizationContext и Console Apps
Await, SynchronizationContext и Console Apps: часть 2

4
chase

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

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

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

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

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

0
Terry Slack

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