簡體   English   中英

在Select linq查詢中使用async / await

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

閱讀這篇文章后: 在Parallel.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);

    });
}

由於某種原因,這行不通...我不明白為什么,我認為這是一個僵局,但我不確定...

因此,在方法末尾, customerTasks包含一個尚未枚舉IEnumerable<Task> Select中的任何代碼都不會運行。

創建此類任務時,立即實現序列以減輕重復枚舉的風險(並意外創建第二批任務)可能更安全。 您可以通過在序列上調用ToList來實現。

所以:

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來做到這Task.WhenAll

await Task.WhenAll(customerTasks);

通過在Select語句中從async委托中實際返回一個值,可以使這一步驟更進一步,最終得到IEnumerable<Task<Customer>>

然后,您可以使用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;
}

加成:

關於何時實際執行LINQ語句,尤其是Where語句,似乎有些混亂。
我創建了一個小程序,以顯示實際何時訪問源數據。此答案末尾的結果

加法結束

您必須了解大多數LINQ函數的惰性。

懶惰的LINQ功能只會更改EnumeratorIEnumerable.GetEnumerator()會當你開始枚舉返回。 因此,只要您調用惰性LINQ函數,查詢就不會執行。

僅當您開始枚舉時,才執行查詢。 當調用foreach或非延遲LINQ函數(例如ToList()Any()FirstOrDefault()Max()Max() ,枚舉開始。

在每個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()一直執行到第一個收益返回語句為止。

每次調用MoveNext()時,都會執行收益返回之后的語句,直到執行下一個收益返回為止。

更改代碼,使之可以使用foreach,Any(),FirstOrDefault(),ToDictionary等訪問枚舉數,這表明對這些函數的調用是實際訪問原始源的時間。

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

調試顯示原始源從頭開始兩次枚舉。 因此,確實這樣做是不明智的,特別是如果您需要做很多事情來計算第一個元素(GroupBy,OrderBy,數據庫訪問等)時,這是不明智的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM