在Select linq查询内使用async/await [重复]

c9x0cxw0  于 2022-12-06  发布在  其他
关注(0)|答案(2)|浏览(200)

此问题在此处已有答案

Async await in linq select(8个答案)
两个月前关门了。
阅读完这篇帖子:正在并行嵌套等待。ForEach
我试着做了以下几点:

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

    });
}

我不明白为什么,我觉得有一个僵局,但我不确定...

wko9yo5t

wko9yo5t1#

所以在方法的末尾,customerTasks包含一个IEnumerable<Task>,这个IEnumerable<Task>还没有被枚举Select中的任何代码都不会运行。
当创建这样的任务时,立即具体化序列可能更安全,以降低双重枚举的风险(并意外创建第二批任务)。
因此:

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

}).ToList();

现在...如何处理您的任务列表?您需要等待它们全部完成...
您可以使用Task.WhenAll执行此操作:

await Task.WhenAll(customerTasks);

您可以更进一步,在Select语句中从async委托返回一个值,这样您就得到了一个IEnumerable<Task<Customer>>
然后可以使用different overload of 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

当然,可能还有更有效的方法来一次性获得几个客户,但这是针对另一个问题。
相反,如果您希望按顺序执行异步任务,则:

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

i7uaboj42#

增加:

对于LINQ语句实际执行的时间,尤其是Where语句,似乎有些混乱。
我创建了一个小程序来显示实际访问源数据的时间。

添加结束

您必须了解大多数LINQ函数的惰性。
惰性LINQ函数只会更改IEnumerable.GetEnumerator()在您开始枚举时返回的Enumerator。因此,只要您调用惰性LINQ函数,查询就不会执行。
只有当你开始枚举的时候,查询才会被执行。当你调用foreach,或者像ToList()Any()FirstOrDefault()Max()这样的LINQ函数的时候,枚举才会开始。
在每个LINQ函数的注解部分都描述了该函数是否为惰性函数。您还可以通过检查返回值来查看该函数是否为惰性函数。如果它返回IEnumerable<...>(或IQueryable),则尚未枚举LINQ。
这种惰性的好处是,只要您只使用惰性函数,更改LINQ表达式就不会耗费时间。只有当您使用非惰性函数时,才必须注意其影响。
例如,如果由于排序、分组、数据库查询等原因,提取序列的第一个元素需要很长时间才能计算,请确保不要开始多次枚举(=不要对同一序列多次使用非惰性函数)

不要在家里这样做

假设您有以下查询

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

此查询仅包含惰性LINQ函数。在所有这些语句之后,尚未访问todoLists
但是,一旦得到结果序列的第一个元素,就必须访问所有元素(可能不止一次),以便按优先级对它们进行分组,计算所涉及的工作时间总数,并按优先级降序对它们进行排序。
Any()和First()都是这种情况:

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

在这种情况下,最好使用正确的函数:

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

回到您问题

Enumerable.Select语句只为您创建了一个IEnumerable对象。您忘记了枚举它。
此外,您还多次构建了CustomerRepo。这是有意的吗?

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

附加:何时执行LINQ语句?

我创建了一个小程序来测试何时执行LINQ语句,特别是当执行Where语句时。
返回IEnumerable的函数:

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

一个使用此枚举的程序,该枚举使用一个老式的枚举器

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

在调试过程中,我可以看到**第一次调用MoveNext()时执行GetNumbers。**GetNumbers()一直执行到第一个yield return语句。
每次调用MoveNext()时,都会执行yield返回之后的语句,直到执行下一个yield返回。
更改代码,以便使用foreach、Any()、FirstOrDefault()、ToDictionary等访问枚举数,这表明对这些函数的调用是实际访问原始源的时间。

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

调试显示原始源**从开头开始枚举两次。**因此,这样做确实是不明智的,尤其是当您需要做很多工作来计算第一个元素(GroupBy、OrderBy、数据库访问等)时。

相关问题