programing tip

비동기 LINQ 쿼리를 작성하는 방법은 무엇입니까?

itbloger 2020. 12. 13. 09:03
반응형

비동기 LINQ 쿼리를 작성하는 방법은 무엇입니까?


많은 LINQ 관련 자료를 읽은 후 갑자기 비동기 LINQ 쿼리 작성 방법을 소개하는 기사가 없다는 것을 깨달았습니다.

LINQ to SQL을 사용한다고 가정하면 아래 설명이 명확합니다. 그러나 SQL 데이터베이스가 느리게 응답하면이 코드 블록을 사용하는 스레드가 방해를받습니다.

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

현재 LINQ 쿼리 사양이 이에 대한 지원을 제공하지 않는 것 같습니다.

비동기 프로그래밍 LINQ를 수행하는 방법이 있습니까? I / O에 대한 차단 지연없이 결과를 사용할 준비가되면 콜백 알림이있는 것처럼 작동합니다.


LINQ에는 실제로이 기능이 없지만 프레임 워크 자체는 수행합니다 ... 자신의 비동기 쿼리 실행기를 30 줄 정도 쉽게 롤링 할 수 있습니다.

편집 : 이것을 작성함으로써 나는 왜 그것을 구현하지 않았는지 발견했습니다. 익명 유형은 로컬 범위이므로 처리 할 수 ​​없습니다. 따라서 콜백 함수를 정의 할 방법이 없습니다. 이것은 많은 linq to sql 항목이 select 절에서 생성하기 때문에 매우 중요한 것입니다. 아래 제안 중 어느 것도 동일한 운명을 겪고 있으므로 여전히 사용하기 가장 쉬운 방법이라고 생각합니다!

편집 : 유일한 해결책은 익명 유형을 사용하지 않는 것입니다. 콜백을 IEnumerable (유형 인수 없음)로만 선언하고 리플렉션을 사용하여 필드에 액세스 할 수 있습니다 (ICK !!). 또 다른 방법은 콜백을 "동적"으로 선언하는 것입니다 ... 오 ... 잠깐 ... 아직 나오지 않았습니다. :) 이것은 동적이 어떻게 사용될 수 있는지에 대한 또 다른 좋은 예입니다. 어떤 사람들은 그것을 학대라고 부를 수 있습니다.

유틸리티 라이브러리에 이것을 던져라 :

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;
        }
    }
}

그리고 다음과 같이 사용할 수 있습니다.

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;
        }
    }

}

Going to go put this up on my blog now, pretty handy.


TheSoftwareJedi's and ulrikb's(aka user316318) solutions are good for any LINQ type, but (as pointed by Chris Moschini) do NOT delegating to underlying asynchronous calls that leverage Windows I/O Completion Ports.

Wesley Bakker's Asynchronous DataContext post (triggered by a blog post of Scott Hanselman ) describe class for LINQ to SQL that uses sqlCommand.BeginExecuteReader/sqlCommand.EndExecuteReader, which leverage Windows I/O Completion Ports.

I/O completion ports provide an efficient threading model for processing multiple asynchronous I/O requests on a multiprocessor system.


Based on Michael Freidgeim's answer and mentioned blog post from Scott Hansellman and fact that you can use async/await, you can implement reusable ExecuteAsync<T>(...) method, which executes underlying SqlCommand asynchronously:

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

And then you can (re)use it like this:

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

I started a simple github project named Asynq to do asynchronous LINQ-to-SQL query execution. The idea is quite simple albeit "brittle" at this stage (as of 8/16/2011):

  1. Let LINQ-to-SQL do the "heavy" work of translating your IQueryable into a DbCommand via the DataContext.GetCommand().
  2. For SQL 200[058], cast up from the abstract DbCommand instance you got from GetCommand() to get a SqlCommand. If you're using SQL CE you're out of luck since SqlCeCommand does not expose the async pattern for BeginExecuteReader and EndExecuteReader.
  3. Use BeginExecuteReader and EndExecuteReader off the SqlCommand using the standard .NET framework asynchronous I/O pattern to get yourself a DbDataReader in the completion callback delegate that you pass to the BeginExecuteReader method.
  4. Now we have a DbDataReader which we have no idea what columns it contains nor how to map those values back up to the IQueryable's ElementType (most likely to be an anonymous type in the case of joins). Sure, at this point you could hand-write your own column mapper that materializes its results back into your anonymous type or whatever. You'd have to write a new one per each query result type, depending on how LINQ-to-SQL treats your IQueryable and what SQL code it generates. This is a pretty nasty option and I don't recommend it since it's not maintainable nor would it be always correct. LINQ-to-SQL can change your query form depending on the parameter values you pass in, for example query.Take(10).Skip(0) produces different SQL than query.Take(10).Skip(10), and perhaps a different resultset schema. Your best bet is to handle this materialization problem programmatically:
  5. "Re-implement" a simplistic runtime object materializer that pulls columns off the DbDataReader in a defined order according to the LINQ-to-SQL mapping attributes of the ElementType Type for the IQueryable. Implementing this correctly is probably the most challenging part of this solution.

As others have discovered, the DataContext.Translate() method does not handle anonymous types and can only map a DbDataReader directly to a properly attributed LINQ-to-SQL proxy object. Since most queries worth writing in LINQ are going to involve complex joins which inevitably end up requiring anonymous types for the final select clause, it's pretty pointless to use this provided watered-down DataContext.Translate() method anyway.

There are a few minor drawbacks to this solution when leveraging the existing mature LINQ-to-SQL IQueryable provider:

  1. You cannot map a single object instance to multiple anonymous type properties in the final select clause of your IQueryable, e.g. from x in db.Table1 select new { a = x, b = x }. LINQ-to-SQL internally keeps track of which column ordinals map to which properties; it does not expose this information to the end user so you have no idea which columns in the DbDataReader are reused and which are "distinct".
  2. You cannot include constant values in your final select clause - these do not get translated into SQL and will be absent from the DbDataReader so you'd have to build custom logic to pull these constant values up from the IQueryable's Expression tree, which would be quite a hassle and is simply not justifiable.

I'm sure there are other query patterns that might break but these are the two biggest I could think of that could cause problems in an existing LINQ-to-SQL data access layer.

These problems are easy to defeat - simply don't do them in your queries since neither pattern provides any benefit to the end result of the query. Hopefully this advice applies to all query patterns that would potentially cause object materialization problems :-P. It's a hard problem to solve not having access to LINQ-to-SQL's column mapping information.

A more "complete" approach to solving the problem would be to effectively re-implement nearly all of LINQ-to-SQL, which is a bit more time-consuming :-P. Starting from a quality, open-source LINQ-to-SQL provider implementation would be a good way to go here. The reason you'd need to reimplement it is so that you'd have access to all of the column mapping information used to materialize the DbDataReader results back up to an object instance without any loss of information.

참고URL : https://stackoverflow.com/questions/252355/how-to-write-asynchronous-linq-query

반응형