Как утилизировать 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 ();
// ...
//}
}
}
}
ОБНОВЛЕНО: закомментированная часть - показать, что что-то будет сделано - асинхронно - с соединением после его открытия, но этот код не требуется для воспроизведения проблемы.
Ответы
Ответ 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();
}
Теперь транзакция распределяется между методами. Посмотрите на ссылку ниже. Это простой пример и более подробно
Для получения полной информации, посмотрите на это