webentwicklung-frage-antwort-db.com.de

Wie entsorge ich TransactionScope in abbrechbarem asynchronem/Warten?

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.

46
chase

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

4
chase

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.

92
ZunTzu

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

0
Terry Slack

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.
    // ...
}
0
Maarten