Ich versuche, die neue asynchrone/Warte-Funktion zu verwenden, um asynchron mit einer Datenbank zu arbeiten. Da einige Anfragen langwierig sein können, möchte ich sie stornieren können. Das Problem, auf das ich stoße, ist, dass TransactionScope
anscheinend eine Thread-Affinität hat, und es scheint, dass beim Abbrechen des Tasks seine Dispose()
auf einem falschen Thread ausgeführt wird.
Wenn ich .TestTx()
aufrufe, erhalte ich die folgende AggregateException
, die InvalidOperationException
für task.Wait ()
enthält:
"A TransactionScope must be disposed on the same thread that it was created."
Hier ist der Code:
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 ();
// ...
//}
}
}
}
AKTUALISIERT: Der auskommentierte Teil soll zeigen, dass - asynchron - mit der Verbindung etwas zu tun ist, sobald sie geöffnet ist. Dieser Code ist jedoch nicht erforderlich, um das Problem zu reproduzieren.
Das Problem rührt von der Tatsache her, dass ich den Code in einer Konsolenanwendung prototypisierte, was ich in der Frage nicht reflektierte.
Die Art und Weise, wie async/await den Code nach await
weiter ausführt, ist vom Vorhandensein von SynchronizationContext.Current
abhängig, und die Konsolenanwendung verfügt standardmäßig nicht über einen Code. Dies bedeutet, dass die Fortsetzung mit der aktuellen TaskScheduler
ausgeführt wird, die eine ThreadPool
ist. möglicherweise? ) wird in einem anderen Thread ausgeführt.
Daher muss einfach eine SynchronizationContext
vorhanden sein, die sicherstellt, dass TransactionScope
in demselben Thread abgelegt wird, in dem sie erstellt wurde. Für WinForms- und WPF-Anwendungen ist dies standardmäßig vorhanden, während Konsolenanwendungen entweder eine benutzerdefinierte verwenden oder DispatcherSynchronizationContext
von WPF ausleihen können.
Hier sind zwei großartige Blogbeiträge, die die Mechanik im Detail erklären:
Warten Sie, SynchronizationContext und Konsolen-Apps
Warten Sie, SynchronizationContext und Konsolen-Apps: Teil 2
In .NET Framework 4.5.1 gibt es eine Reihe von neuen Konstruktoren für TransactionScope , die den Parameter TransactionScopeAsyncFlowOption annehmen.
Laut MSDN ermöglicht es den Transaktionsfluss über Thread-Fortsetzungen.
Mein Verständnis ist, dass es Ihnen erlaubt ist, Code wie folgt zu schreiben:
// 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();
}
Ich habe es noch nicht ausprobiert, daher weiß ich nicht, ob es funktionieren wird.
Ich weiß, dass dies ein alter Thread ist, aber wenn jemand auf das Problem gestoßen ist. System.InvalidOperationException: Ein TransactionScope muss sich in demselben Thread befinden, in dem er erstellt wurde.
Die Lösung besteht darin, mindestens ein Upgrade auf .net 4.5.1 durchzuführen und eine Transaktion wie die folgende zu verwenden:
using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
//Run some code here, like calling an async method
await someAsnycMethod();
transaction.Complete();
}
Jetzt wird die Transaktion zwischen den Methoden aufgeteilt. Schauen Sie sich den Link unten an. Es bietet ein einfaches Beispiel und mehr Details
Um vollständige Details zu erfahren, schauen Sie unter This
Ja, Sie müssen Ihren Transaktionsbereich in einem einzigen Thread halten. Da Sie den Transaktionsbereich vor der asynchronen Aktion erstellen und in der asynchronen Aktion verwenden, wird der Transaktionsbereich nicht in einem einzelnen Thread verwendet. Der TransactionScope wurde nicht für diese Verwendung entwickelt.
Ich denke, eine einfache Lösung wäre, die Erstellung des TransactionScope-Objekts und des Verbindungsobjekts in die async-Aktion zu verschieben.
AKTUALISIEREN
Da sich die async-Aktion innerhalb des SqlConnection-Objekts befindet, können wir das nicht ändern. Was wir tun können, ist die Verbindung in den Transaktionsbereich aufzunehmen . Ich würde das Verbindungsobjekt asynchron erstellen und dann den Transaktionsbereich erstellen und die Transaktion eintragen.
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.
// ...
}