2008-10-31 12 views
56

Después de leer un montón de cosas relacionadas con LINQ, de repente me di cuenta de que ningún artículo presentaba cómo escribir consultas LINQ asíncronas.Cómo escribir consultas asincrónicas LINQ?

Supongamos que usamos LINQ to SQL, la siguiente declaración es clara. Sin embargo, si la base de datos SQL responde lentamente, el hilo que utiliza este bloque de código se verá obstaculizado.

var result = from item in Products where item.Price > 3 select item.Name; 
foreach (var name in result) 
{ 
    Console.WriteLine(name); 
} 

Parece que la especificación actual de consulta LINQ no proporciona ayuda para esto.

¿Hay alguna manera de hacer la programación asincrónica LINQ? Funciona como si hubiera una notificación de devolución de llamada cuando los resultados están listos para usar sin ningún retraso de bloqueo en E/S.

+0

¿Funcionó bien la respuesta de abajo para usted? – TheSoftwareJedi

+0

Consulte las extensiones reactivas para .NET en http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx, está diseñado para consultas Linq asincrónicas. –

+2

De hecho, pero no Linq * a SQL * consultas que es lo que el autor está preguntando. –

Respuesta

34

Si bien LINQ realmente no tiene esto en sí mismo, el marco en sí sí ... Puede ejecutar fácilmente su propio ejecutor de consultas asincrónicas en 30 líneas más o menos ... De hecho, acabo de lanzar esto para usted:)

EDITAR: Al escribir esto, he descubierto por qué no lo implementaron. No puede manejar tipos anónimos ya que tienen un alcance local. Por lo tanto, no tiene forma de definir su función de devolución de llamada. Esto es algo muy importante ya que muchas cosas de linq a sql las crean en la cláusula de selección. Cualquiera de las siguientes sugerencias sufre el mismo destino, ¡así que todavía creo que esta es la más fácil de usar!

EDITAR: La única solución es no utilizar tipos anónimos. Puede declarar la devolución de llamada simplemente tomando IEnumerable (sin args de tipo), y use reflection para acceder a los campos (ICK !!). Otra forma sería declarar la devolución de llamada como "dinámica" ... oh ... espera ... Aún no ha salido. :) Este es otro ejemplo decente de cómo se podría utilizar la dinámica. Algunos pueden llamarlo abuso.

tirar esto en la biblioteca de utilidades:

public static class AsynchronousQueryExecutor 
{ 
    public static void Call<T>(IEnumerable<T> query, Action<IEnumerable<T>> callback, Action<Exception> errorCallback) 
    { 
     Func<IEnumerable<T>, IEnumerable<T>> func = 
      new Func<IEnumerable<T>, IEnumerable<T>>(InnerEnumerate<T>); 
     IEnumerable<T> result = null; 
     IAsyncResult ar = func.BeginInvoke(
          query, 
          new AsyncCallback(delegate(IAsyncResult arr) 
          { 
           try 
           { 
            result = ((Func<IEnumerable<T>, IEnumerable<T>>)((AsyncResult)arr).AsyncDelegate).EndInvoke(arr); 
           } 
           catch (Exception ex) 
           { 
            if (errorCallback != null) 
            { 
             errorCallback(ex); 
            } 
            return; 
           } 
           //errors from inside here are the callbacks problem 
           //I think it would be confusing to report them 
           callback(result); 
          }), 
          null); 
    } 
    private static IEnumerable<T> InnerEnumerate<T>(IEnumerable<T> query) 
    { 
     foreach (var item in query) //the method hangs here while the query executes 
     { 
      yield return item; 
     } 
    } 
} 

y se podía utilizar de esta manera:

class Program 
{ 

    public static void Main(string[] args) 
    { 
     //this could be your linq query 
     var qry = TestSlowLoadingEnumerable(); 

     //We begin the call and give it our callback delegate 
     //and a delegate to an error handler 
     AsynchronousQueryExecutor.Call(qry, HandleResults, HandleError); 

     Console.WriteLine("Call began on seperate thread, execution continued"); 
     Console.ReadLine(); 
    } 

    public static void HandleResults(IEnumerable<int> results) 
    { 
     //the results are available in here 
     foreach (var item in results) 
     { 
      Console.WriteLine(item); 
     } 
    } 

    public static void HandleError(Exception ex) 
    { 
     Console.WriteLine("error"); 
    } 

    //just a sample lazy loading enumerable 
    public static IEnumerable<int> TestSlowLoadingEnumerable() 
    { 
     Thread.Sleep(5000); 
     foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }) 
     { 
      yield return i; 
     } 
    } 

} 

va a ir a poner esto en mi blog ahora, bastante práctico.

+0

¿No sería posible incorporar fundición de algún tipo? Como este enlace: http://tomasp.net/articles/cannot-return-anonymous-type-from-method.aspx. –

+3

¿Efectivamente estás pasando el bloque a otro hilo en el grupo de hilos? o hay algo de magia pasando aquí? –

+22

Este es un error común en .Net: aplicar el patrón IAsync sobre el código síncrono en lugar de delegar en llamadas asincrónicas subyacentes que aprovechan los puertos de finalización de E/S de Windows para entregar el tiempo de espera y no bloquear ningún hilo. Como Harry señala, esto le da el bloqueo a otro hilo de ThreadPool. Esto solo tiene el beneficio menor de que el hilo iniciador sea libre de ejecutar otro trabajo secundario. Este enfoque puede bloquear todos los subprocesos de ThreadPool si hay suficientes solicitudes simultáneas que dan lugar a consultas largas. –

12

TheSoftwareJedi de ulrikb y 's (aka) user316318 soluciones son buenas para cualquier tipo de LINQ, pero (como se ha señalado por Chris Moschini) no delegando en asíncrono subyacente llamadas que Puertos o Finalización/apalancamiento de Windows I.

Asynchronous DataContext posterior de Wesley Bakker (provocada por a blog post of Scott Hanselman) describen la clase de LINQ to SQL que utiliza sqlCommand.BeginExecuteReader/sqlCommand.EndExecuteReader, que aprovechan los puertos o Finalización de Windows Me /.

I/O completion ports proporcionan un modelo de subprocesamiento eficiente para procesar múltiples solicitudes de E/S asíncronas en un sistema multiprocesador.

+0

Esta debería ser realmente la respuesta aceptada, ya que realmente especifica cómo realizar las operaciones de una manera asíncrona adecuada que beneficiará al programa al no tomar un hilo esperando que se complete la operación. –

3

Inicié un proyecto simple de github llamado Asynq para realizar una ejecución asíncrona de consultas LINQ-to-SQL. La idea es bastante simple aunque "frágil" en esta etapa (a partir del 8/16/2011):

  1. Vamos LINQ a SQL hacer el trabajo "pesado" de la traducción de su IQueryable en un DbCommand a través de la DataContext.GetCommand() .
  2. Para SQL 200 [058], creado a partir de la instancia abstracta DbCommand que obtuvo de GetCommand() para obtener un . Si está utilizando SQL CE, no tiene suerte, ya que SqlCeCommand no expone el patrón asíncrono para BeginExecuteReader y EndExecuteReader.
  3. Uso BeginExecuteReader y fuera de la EndExecuteReaderSqlCommand utilizando el estándar asíncrono marco .NET patrón de E/S para conseguirse un DbDataReader en la realización de devolución de llamada delegado que se pasa al método BeginExecuteReader.
  4. Ahora tenemos un DbDataReader que no tenemos idea de qué columnas contiene ni cómo volver a asignar esos valores al IQueryable 's ElementType (lo más probable es que sea un tipo anónimo en el caso de las uniones). Claro, en este punto podría escribir a mano su propio mapeador de columnas que materializa sus resultados en su tipo anónimo o lo que sea. Tendría que escribir uno nuevo para cada tipo de resultado de consulta, dependiendo de cómo trate LINQ-to-SQL su IQueryable y el código SQL que genera. Esta es una opción bastante desagradable y no la recomiendo, ya que no es mantenible ni tampoco será siempre correcta. LINQ-to-SQL puede cambiar su formulario de consulta en función de los valores de parámetros que pase, por ejemplo query.Take(10).Skip(0) produce SQL diferente de query.Take(10).Skip(10), y tal vez un esquema de conjunto de resultados diferente. Su mejor opción es manejar este problema de materialización mediante programación:
  5. "Reimplementar" un materializador de objetos de tiempo de ejecución simplificado que extrae columnas del DbDataReader en un orden definido de acuerdo con los atributos de mapeo LINQ-to-SQL del tipo ElementType para el IQueryable. Implementar esto correctamente es probablemente la parte más desafiante de esta solución.

Como otros han descubierto, el método DataContext.Translate() no controla los tipos anónimos y sólo se puede asignar una DbDataReader directamente a un objeto proxy correctamente atribuido LINQ a SQL. Dado que la mayoría de las consultas que vale la pena escribir en LINQ van a involucrar combinaciones complejas que inevitablemente requieren tipos anónimos para la cláusula de selección final, no tiene sentido usar el método diluido DataContext.Translate() suministrado de todos modos.

Hay algunos inconvenientes menores a esta solución cuando aprovechan la madura proveedor de IQueryable LINQ a SQL existente:

  1. no se puede asignar una única instancia de objeto en varias propiedades de tipo anónimo en la cláusula select final su IQueryable, por ejemplo from x in db.Table1 select new { a = x, b = x }. LINQ-to-SQL realiza un seguimiento interno de los ordinales de columna que se asignan a qué propiedades; no expone esta información al usuario final, por lo que no tiene idea de qué columnas del DbDataReader se reutilizan y cuáles son "distintas".
  2. No puede incluir valores constantes en la cláusula de selección final; estos no se traducen a SQL y estarán ausentes del DbDataReader, por lo que tendría que crear lógica personalizada para obtener estos valores constantes del árbol IQueryableExpression , lo cual sería bastante complicado y simplemente no es justificable.

Estoy seguro de que hay otros patrones de consulta que podrían romperse pero estos son los dos más grandes que podría pensar que podrían causar problemas en una capa existente de acceso a datos LINQ-a-SQL.

Estos problemas son fáciles de vencer, simplemente no los haga en sus consultas ya que ninguno de los patrones proporciona ningún beneficio al resultado final de la consulta. Esperamos que este consejo se aplique a todos los patrones de consulta que podrían causar problemas de materialización del objeto :-P.Es un problema difícil de resolver al no tener acceso a la información de asignación de columnas de LINQ-to-SQL.

Un enfoque más "completo" para resolver el problema sería implementar de nuevo de forma efectiva casi todo LINQ-to-SQL, lo que requiere un poco más de tiempo :-P. A partir de una implementación de proveedores de LINQ-a-SQL de código abierto de calidad sería una buena manera de hacerlo aquí. La razón por la que deberá volver a implementarla es para tener acceso a toda la información de la asignación de columnas utilizada para materializar los resultados de DbDataReader en una instancia de objeto sin pérdida de información.

+1

Olvidé mencionar que este proyecto github es MUCHO un TRABAJO EN CURSO. Yo activamente te disuadio de usarlo en CUALQUIER código de producción que sea. Es un proyecto de investigación simple en esta etapa utilizado para demostrar la idea de que se puede hacer. –

+0

Un ejemplo de código con una solución similar. http://www.hanselman.com/blog/TheWeeklySourceCode51AsynchronousDatabaseAccessAndLINQToSQLFun.aspx –

4

Basado en Michael Freidgeim's answer y mencionó blog post from Scott Hansellman y que se puede utilizar async/await, puede implementar reutilizable ExecuteAsync<T>(...) método, que ejecuta subyacente SqlCommand de forma asíncrona:

protected static async Task<IEnumerable<T>> ExecuteAsync<T>(IQueryable<T> query, 
    DataContext ctx, 
    CancellationToken token = default(CancellationToken)) 
{ 
    var cmd = (SqlCommand)ctx.GetCommand(query); 

    if (cmd.Connection.State == ConnectionState.Closed) 
     await cmd.Connection.OpenAsync(token); 
    var reader = await cmd.ExecuteReaderAsync(token); 

    return ctx.Translate<T>(reader); 
} 

Y a continuación, puede (re) usarlo como esto:

public async Task WriteNamesToConsoleAsync(string connectionString, CancellationToken token = default(CancellationToken)) 
{ 
    using (var ctx = new DataContext(connectionString)) 
    { 
     var query = from item in Products where item.Price > 3 select item.Name; 
     var result = await ExecuteAsync(query, ctx, token); 
     foreach (var name in result) 
     { 
      Console.WriteLine(name); 
     } 
    } 
} 
+0

Esta es la solución más limpia hasta ahora para consultar los objetos, cubre la mayoría de los escenarios y es bastante ordenada. Pero, ¿hay alguna forma de lograr un Async SubmitChanges? Por ejemplo, para insertar un nuevo registro en una tabla. – ogggre