简体   繁体   English

在Select linq查询中使用async / await

[英]Using async/await inside a Select linq query

After reading this post: Nesting await in Parallel.ForEach 阅读这篇文章后: 在Parallel.ForEach中嵌套等待

I tried to do the following: 我尝试执行以下操作:

private static async void Solution3UsingLinq()
{
    var ids = new List<string>() { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };

    var customerTasks = ids.Select(async i =>
    {
        ICustomerRepo repo = new CustomerRepo();
        var id = await repo.getCustomer(i);
        Console.WriteLine(id);

    });
}

For some reason, this doesn't work... I don't understand why, I think there is a deadlock but i'm not sure... 由于某种原因,这行不通...我不明白为什么,我认为这是一个僵局,但我不确定...

So at the end of your method, customerTasks contains an IEnumerable<Task> that has not been enumerated . 因此,在方法末尾, customerTasks包含一个尚未枚举IEnumerable<Task> None of the code within the Select even runs. Select中的任何代码都不会运行。

When creating tasks like this, it's probably safer to materialize your sequence immediately to mitigate the risk of double enumeration (and creating a second batch of tasks by accident). 创建此类任务时,立即实现序列以减轻重复枚举的风险(并意外创建第二批任务)可能更安全。 You can do this by calling ToList on your sequence. 您可以通过在序列上调用ToList来实现。

So: 所以:

var customerTasks = ids.Select(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var id = await repo.getCustomer(i); //consider changing to GetCustomerAsync
    Console.WriteLine(id);

}).ToList();

Now... what to do with your list of tasks? 现在...如何处理您的任务列表? You need to wait for them all to complete... 您需要等待它们全部完成...

You can do this with Task.WhenAll : 您可以使用Task.WhenAll来做到这Task.WhenAll

await Task.WhenAll(customerTasks);

You could take this a step further by actually returning a value from your async delegate in the Select statement, so you end up with an IEnumerable<Task<Customer>> . 通过在Select语句中从async委托中实际返回一个值,可以使这一步骤更进一步,最终得到IEnumerable<Task<Customer>>

Then you can use a different overload of Task.WhenAll : 然后,您可以使用Task.WhenAll其他重载

IEnumerable<Task<Customer>> customerTasks = ids.Select(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var c = await repo.getCustomer(i); //consider changing to GetCustomerAsync
    return c;

}).ToList();

Customer[] customers = await Task.WhenAll(customerTasks); //look... all the customers

Of course, there are probably more efficient means of getting several customers in one go, but that would be for a different question. 当然,可能有更有效的方法来一次性吸引多个客户,但这将是一个不同的问题。

If instead, you'd like to perform your async tasks in sequence then: 如果相反,您想按顺序执行异步任务,则:

var customerTasks = ids.Select(async i =>
{
    ICustomerRepo repo = new CustomerRepo();
    var id = await repo.getCustomer(i); //consider changing to GetCustomerAsync
    Console.WriteLine(id);

});
foreach(var task in customerTasks) //items in sequence will be materialized one-by-one
{
    await task;
}

Addition: 加成:

There seems to be some confusion about when the the LINQ statements actually are executed, especially the Where statement. 关于何时实际执行LINQ语句,尤其是Where语句,似乎有些混乱。
I created a small program to show when the source data actually is accessed Results at the end of this answer 我创建了一个小程序,以显示实际何时访问源数据。此答案末尾的结果

end of Addition 加法结束

You have to be aware about the lazyness of most LINQ functions. 您必须了解大多数LINQ函数的惰性。

Lazy LINQ functions will only change the Enumerator that IEnumerable.GetEnumerator() will return when you start enumerating. 懒惰的LINQ功能只会更改EnumeratorIEnumerable.GetEnumerator()会当你开始枚举返回。 Hence, as long as you call lazy LINQ functions the query isn't executed. 因此,只要您调用惰性LINQ函数,查询就不会执行。

Only when you starte enumerating, the query is executed. 仅当您开始枚举时,才执行查询。 Enumerating starts when you call foreach , or non-layzy LINQ functions like ToList() , Any() , FirstOrDefault() , Max() , etc. 当调用foreach或非延迟LINQ函数(例如ToList()Any()FirstOrDefault()Max()Max() ,枚举开始。

In the comments section of every LINQ function is described whether the function is lazy or not. 在每个LINQ函数的注释部分中,描述了该函数是否是惰性的。 You can also see whether the function is lazy by inspecting the return value. 您还可以通过检查返回值来查看函数是否是惰性的。 If it returns an IEnumerable<...> (or IQueryable) the LINQ is not enumerated yet. 如果返回IEnumerable <...>(或IQueryable),则尚未枚举LINQ。

The nice thing about this lazyness, is that as long as you use only lazy functions, changing the LINQ expression is not time consuming. 关于这种惰性的好处是,只要您仅使用惰性函数,更改LINQ表达式就不会浪费时间。 Only when you use non-lazy functions, you have to be aware of the impact. 仅当您使用非惰性函数时,您才需要注意其影响。

For instance, if fetching the first element of a sequence takes a long time to calculate, because of Ordering, Grouping, Database queries etc, make sure you don't start enumerating more then once (= don't use non-lazy functions for the same sequence more than once) 例如,如果由于顺序,分组,数据库查询等原因,要提取序列的第一个元素需要很长时间才能计算出来,请确保不要再枚举一次(=请勿将非惰性函数用于同一序列不止一次)

Don't do this at home: 不要在家中这样做:

Suppose you have the following query 假设您有以下查询

var query = toDoLists
    .Where(todo => todo.Person == me)
    .GroupBy(todo => todo.Priority)
    .Select(todoGroup => new
    {
        Priority = todoGroup.Key,
        Hours = todoGroup.Select(todo => todo.ExpectedWorkTime).Sum(),
     }
     .OrderByDescending(work => work.Priority)
     .ThenBy(work => work.WorkCount);

This query contains only lazy LINQ functions. 该查询仅包含惰性LINQ函数。 After all these statement, the todoLists have not been accessed yet. 所有这些语句之后,尚未访问todoLists

But as soon as you get the first element of the resulting sequence all elements have to be accessed (probably more than once) to group them by priority, calculate the total number of involved working hours and to sort them by descending priority. 但是,一旦获得结果序列的第一个元素,就必须访问所有元素(可能不止一次)以按优先级对它们进行分组,计算涉及的工作时间总数并按降序对它们进行排序。

This is the case for Any(), and again for First(): Any()和First()都是这种情况:

if (query.Any())                           // do grouping, summing, ordering
{
    var highestOnTodoList = query.First(); // do all work again
    Process(highestOnTodoList);
}
else
{   // nothing to do
    GoFishing();
}

In such cases it is better to use the correct function: 在这种情况下,最好使用正确的功能:

var highestOnToDoList = query.FirstOrDefault(); // do grouping / summing/ ordering
if (highestOnTioDoList != null)
   etc.

back to your question 回到你的问题

The Enumerable.Select statement only created an IEnumerable object for you. Enumerable.Select语句仅为您创建了IEnumerable对象。 You forgot to enumerate over it. 您忘记枚举了。

Besides you constructed your CustomerRepo several times. 此外,您还多次构造了CustomerRepo。 Was that intended? 那是故意的吗?

ICustomerRepo repo = new CustomerRepo();
IEnumerable<Task<CustomerRepo>> query = ids.Select(id => repo.getCustomer(i));

foreach (var task in query)
{
     id = await task;
     Console.WriteLine(id);
}

Addition: when are the LINQ statements executed? 另外:LINQ语句何时执行?

I created a small program to test when a LINQ statement is executed, especially when a Where is executed. 我创建了一个小程序来测试LINQ语句何时执行,尤其是执行Where时。

A function that returns an IEnumerable: 返回IEnumerable的函数:

IEnumerable<int> GetNumbers()
{
    for (int i=0; i<10; ++i)
    {
        yield return i;
    }
}

A program that uses this enumeration using an old fashioned Enumerator 一个使用老式枚举器使用此枚举的程序

public static void Main()
{
    IEnumerable<int> number = GetNumbers();
    IEnumerable<int> smallNumbers = numbers.Where(number => number < 3);

    IEnumerator<int> smallEnumerator = smallNumbers.GetEnumerator();

    bool smallNumberAvailable = smallEnumerator.MoveNext();
    while (smallNumberAvailable)
    {
        int smallNumber = smallEnumerator.Current;
        Console.WriteLine(smallNumber);
        smallNumberAvailable = smallEnumerator.MoveNext();
    }
}

During debugging I can see that the first time GetNumbers is executed the first time that MoveNext() is called. 在调试期间,我可以看到第一次调用MoveNext()时第一次执行GetNumbers。 GetNumbers() is executed until the first yield return statement. GetNumbers()一直执行到第一个收益返回语句为止。

Every time that MoveNext() is called the statements after the yield return are performed until the next yield return is executed. 每次调用MoveNext()时,都会执行收益返回之后的语句,直到执行下一个收益返回为止。

Changing the code such that the enumerator is accessed using foreach, Any(), FirstOrDefault(), ToDictionary, etc, shows that the calls to these functions are the time that the originating source is actually accessed. 更改代码,使之可以使用foreach,Any(),FirstOrDefault(),ToDictionary等访问枚举数,这表明对这些函数的调用是实际访问原始源的时间。

if (smallNumbers.Any())
{
    int x = smallNumbers.First();
    Console.WriteLine(x);
}

Debugging shows that the originating source starts enumerating from the beginning twice. 调试显示原始源从头开始两次枚举。 So indeed, it is not wise to do this, especially if you need to do a lot to calculate the first element (GroupBy, OrderBy, Database access, etc) 因此,确实这样做是不明智的,特别是如果您需要做很多事情来计算第一个元素(GroupBy,OrderBy,数据库访问等)时,这是不明智的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM