2012-10-04 4 views
36

Ich versuche, die neue async/await-Funktion zu verwenden, um asynchron mit einer DB zu arbeiten. Da einige der Anfragen langwierig sein können, möchte ich sie abbrechen können. Das Problem, das ich renne ist, dass TransactionScope offenbar eine Thread-Affinität hat, und es scheint, dass beim Abbrechen der Aufgabe, Dispose() wird auf einem falschen Thread gelaufen.Wie man TransactionScope in annullierbar async/warten?

Insbesondere wenn .TestTx() Aufruf erhalte ich die folgende AggregateException enthält InvalidOperationException auf task.Wait():

"A TransactionScope must be disposed on the same thread that it was created." 

Hier ist der Code ist:

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: die auf Kommentar Teil etwas zu zeigen, ist es zu sein asynchron - mit der Verbindung, sobald es geöffnet ist, aber dieser Code ist nicht erforderlich, das Problem zu reproduzieren.

Antwort

4

Das Problem ergibt sich aus der Tatsache, dass ich den Code in eine Konsolenanwendung wurde das Prototyping, die ich nicht in der Frage widerspiegelten.

Die Art und Weise Async/erwarten weiterhin den Code auszuführen, nachdem await auf dem Vorhandensein von SynchronizationContext.Current abhängt und Konsolenanwendung kann ein standardmäßig nicht haben, die die Fortsetzung bedeutet, wird TaskScheduler unter Verwendung der aktuellen ausgeführt, die eine ThreadPool ist , so dass es (möglicherweise?) auf einem anderen Thread ausgeführt wird.

So muss man einfach eine SynchronizationContext haben, die sicherstellen wird, TransactionScope ist auf dem gleichen Thread angeordnet wurde es erstellt wurde. WinForms- und WPF-Anwendungen haben dies standardmäßig, während Konsolenanwendungen entweder eine benutzerdefinierte verwenden oder DispatcherSynchronizationContext von WPF ausleihen können.

hier zwei große Blog-Beiträge sind, die die Mechanik im Detail zu erklären:
Await, SynchronizationContext, and Console Apps
Await, SynchronizationContext, and Console Apps: Part 2

0

Ja, Sie müssen Sie transactioncope auf einem einzigen Thread halten. Da Sie den transactioncope vor der async-Aktion erstellen und ihn in der async-Aktion verwenden, wird der transactionscope nicht in einem einzelnen Thread verwendet. Das TransactionScope wurde nicht so entwickelt, dass es verwendet werden kann.

Eine einfache Lösung Ich denke, würde die Erstellung des TransactionScope-Objekts und das Connection-Objekt in die asynchrone Aktion verschieben.

UPDATE

Da die Asynchron-Aktion innerhalb des SqlConnection-Objekt ist, können wir nicht ändern. Was wir tun können, ist enlist the connection in the transaction scope. Ich würde das Verbindungsobjekt asynchron erstellen und dann den Transaktionsbereich erstellen und die Transaktion registrieren.

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

Können Sie Ihren zweiten Punkt näher erläutern? Verschieben Sie die Kreation wo? – chase

+0

Würden Sie dann vorschlagen, die Verbindung nur asynchron zu öffnen und blockierende Aufrufe für die tatsächliche Arbeitsauslastung zu verwenden? Oder verpasse ich noch etwas? – chase

+0

Im Moment in Ihrer Frage erwerben Sie nur die Verbindung asynchron. Ich bin Ihrem Beispiel gefolgt, aber wenn Sie Ihre Frage mit einer tatsächlichen Arbeitslast aktualisieren, kann ich auch meine Antwort aktualisieren. – Maarten

76

In .NET Framework 4.5.1 gibt es eine Reihe von new constructors for TransactionScope, die ein TransactionScopeAsyncFlowOption Parameter nehmen.

Gemäß der MSDN ermöglicht es den Transaktionsfluss über Thread-Fortsetzungen.

Mein Verständnis ist, dass es gemeint ist, damit Sie Code wie folgt 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, so dass ich weiß nicht, ob es funktionieren wird.

+2

Versucht und es funktioniert. EF6 und Transaktionsumfang oben verwenden –

+0

Perfekt, danke! –

+0

Was ist, wenn Sie nicht auf 4.5.1 upgraden können? Was ist die Lösung dann? – JobaDiniz