2012-10-04 77 views
36

Estoy tratando de usar la nueva función async/await para trabajar asincrónicamente con un DB. Como algunas de las solicitudes pueden ser largas, quiero poder cancelarlas. El problema al que me estoy enfrentando es que TransactionScope aparentemente tiene una afinidad de subprocesos, y parece que al cancelar la tarea, su Dispose() se ejecuta en un subproceso incorrecto.¿Cómo deshacerse de TransactionScope en async/await cancelable?

En concreto, cuando se llama a .TestTx() me sale el siguiente AggregateException contiene InvalidOperationException en task.Wait():

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

Aquí está el código:

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

ACTUALIZADO: la parte comentada es mostrar que hay algo para estar hecho - asincrónicamente - con la conexión una vez que está abierta, pero ese código no es necesario para reproducir el problema.

Respuesta

4

El problema se deriva del hecho de que estaba Prototyping el código en una aplicación de consola, que no reflejaba en la pregunta.

La forma asíncrona/espera continúa ejecutando el código después de await depende de la presencia de SynchronizationContext.Current, y la aplicación de consola no tiene uno por defecto, lo que significa que la continuación será ejecutado usando la corriente TaskScheduler, que es un ThreadPool , entonces (potencialmente?) se ejecuta en un hilo diferente.

Por lo tanto, uno solo necesita tener un SynchronizationContext que asegurará que TransactionScope esté dispuesto en el mismo hilo que se creó. Las aplicaciones WinForms y WPF lo tendrán de forma predeterminada, mientras que las aplicaciones de consola pueden usar una personalizada o tomar DispatcherSynchronizationContext de WPF.

Éstos son dos grandes entradas de blog que explican la mecánica en detalle:
Await, SynchronizationContext, and Console Apps
Await, SynchronizationContext, and Console Apps: Part 2

0

Sí, tiene que mantener su transactioncope en un solo hilo. Dado que está creando el scope de transacciones antes de la acción asíncrona y lo usa en la acción asíncrona, el scope de transacciones no se usa en un solo subproceso. El TransactionScope no fue diseñado para ser utilizado de esa manera.

Una solución simple, creo que sería mover la creación del objeto TransactionScope y el objeto Connection en la acción asincrónica.

ACTUALIZACIÓN

Dado que la acción asíncrono está dentro del objeto SqlConnection, no podemos alterar eso. Lo que podemos hacer es enlist the connection in the transaction scope. Yo crearía el objeto de conexión de manera asíncrona, y luego crearía el alcance de la transacción, y alistaría la transacción.

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

¿Podría profundizar en su segundo punto? Mueve la creación a dónde? – chase

+0

¿Está sugiriendo que solo abra la conexión de forma asincrónica y use llamadas de bloqueo para la carga de trabajo real? ¿O me estoy perdiendo algo de nuevo? – chase

+0

Por el momento en su pregunta solo adquiere la conexión de forma asíncrona. He seguido su ejemplo, pero si actualiza su pregunta con cierta carga de trabajo real, entonces también puedo actualizar mi respuesta. – Maarten

76

En .NET Framework 4.5.1, hay un conjunto de new constructors for TransactionScope que toma un parámetro TransactionScopeAsyncFlowOption.

De acuerdo con MSDN, permite el flujo de transacciones a través de continuaciones de subprocesos.

Mi entendimiento es que está destinado a permitir escribir código como este:

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

no he probado todavía, así que no sé si va a trabajar.

+2

Probado y funciona. Usando EF6 y alcance de transacción en la parte superior. –

+0

Perfecto, gracias! –

+0

¿Qué sucede si no puede actualizar a 4.5.1? ¿Cuál es la solución entonces? – JobaDiniz