Làm cách nào để chờ danh sách các tác vụ không đồng bộ bằng LINQ?


87

Tôi có một danh sách các nhiệm vụ mà tôi đã tạo như sau:

public async Task<IList<Foo>> GetFoosAndDoSomethingAsync()
{
    var foos = await GetFoosAsync();

    var tasks = foos.Select(async foo => await DoSomethingAsync(foo)).ToList();

    ...
}

Bằng cách sử dụng .ToList(), tất cả các tác vụ sẽ bắt đầu. Bây giờ tôi muốn chờ họ hoàn thành và trả về kết quả.

Điều này hoạt động trong ...khối trên :

var list = new List<Foo>();
foreach (var task in tasks)
    list.Add(await task);
return list;

Nó làm những gì tôi muốn, nhưng điều này có vẻ khá vụng về. Tôi muốn viết một cái gì đó đơn giản hơn như thế này:

return tasks.Select(async task => await task).ToList();

... nhưng điều này không biên dịch. Tôi đang thiếu gì? Hay chỉ là không thể diễn đạt mọi thứ theo cách này?


Bạn cần xử lý DoSomethingAsync(foo)tuần tự cho từng foo hay đây là ứng cử viên cho Parallel.ForEach <Foo> ?
mdisibio

1
@mdisibio - Parallel.ForEachđang chặn. Mô hình ở đây đến từ video C # không đồng bộ của Jon Skeet trên Pluralsight . Nó thực hiện song song mà không bị chặn.
Matt Johnson-Pint

@mdisibio - Không. Chúng chạy song song. Hãy thử nó . (Ngoài ra, có vẻ như tôi không cần .ToList()nếu tôi chỉ cần đi để sử dụng WhenAll.)
Matt Johnson-Pint

Đã thực hiện. Tùy thuộc vào cách DoSomethingAsyncđược viết, danh sách có thể được thực thi song song hoặc không. Tôi đã có thể viết một phương thức thử nghiệm đã có và một phiên bản không, nhưng trong cả hai trường hợp, hành vi được quyết định bởi chính phương thức, không phải do người ủy quyền tạo tác vụ. Xin lỗi cho sự pha trộn lên. Tuy nhiên, nếu DoSomethingAsyctrả về Task<Foo>, thì điều đó awaitkhông hoàn toàn cần thiết ... Tôi nghĩ đó là điểm chính mà tôi sẽ cố gắng thực hiện.
mdisibio

Câu trả lời:


136

LINQ không hoạt động hoàn hảo với asyncmã, nhưng bạn có thể làm điều này:

var tasks = foos.Select(DoSomethingAsync).ToList();
await Task.WhenAll(tasks);

Nếu tất cả các tác vụ của bạn đều trả về cùng một loại giá trị, thì bạn thậm chí có thể thực hiện điều này:

var results = await Task.WhenAll(tasks);

đó là khá tốt đẹp. WhenAlltrả về một mảng, vì vậy tôi tin rằng phương thức của bạn có thể trả về kết quả trực tiếp:

return await Task.WhenAll(tasks);

11
Chỉ muốn chỉ ra rằng điều này cũng có thể làm việc vớivar tasks = foos.Select(foo => DoSomethingAsync(foo)).ToList();
mdisibio

1
hoặc thậm chívar tasks = foos.Select(DoSomethingAsync).ToList();
Todd Menier

3
lý do đằng sau nó là gì mà Linq không hoạt động hoàn hảo với mã không đồng bộ?
Ehsan Sajjad

2
@EhsanSajjad: Vì LINQ to Objects hoạt động đồng bộ trên các đối tượng trong bộ nhớ. Một số thứ hạn chế hoạt động, như Select. Nhưng hầu hết không, giống như Where.
Stephen Cleary,

4
@EhsanSajjad: Nếu hoạt động dựa trên I / O, thì bạn có thể sử dụng asyncđể giảm luồng; nếu nó bị ràng buộc bởi CPU và đã ở trên một luồng nền thì asyncsẽ không mang lại bất kỳ lợi ích nào.
Stephen Cleary

9

Để mở rộng câu trả lời của Stephen, tôi đã tạo phương pháp mở rộng sau để giữ phong cách trôi chảy của LINQ. Sau đó bạn có thể làm

await someTasks.WhenAll()

namespace System.Linq
{
    public static class IEnumerableExtensions
    {
        public static Task<T[]> WhenAll<T>(this IEnumerable<Task<T>> source)
        {
            return Task.WhenAll(source);
        }
    }
}

10
Theo cá nhân tôi, tôi sẽ đặt tên cho phương thức mở rộng của bạnToArrayAsync
torvin

4

Một vấn đề với Task.WhenAll là nó sẽ tạo ra một sự song song. Trong hầu hết các trường hợp, nó có thể tốt hơn, nhưng đôi khi bạn muốn tránh nó. Ví dụ, đọc dữ liệu hàng loạt từ DB và gửi dữ liệu đến một số dịch vụ web từ xa. Bạn không muốn tải tất cả các lô vào bộ nhớ nhưng hãy nhấn DB khi lô trước đó đã được xử lý. Vì vậy, bạn phải phá vỡ sự không đồng bộ. Đây là một ví dụ:

var events = Enumerable.Range(0, totalCount/ batchSize)
   .Select(x => x*batchSize)
   .Select(x => dbRepository.GetEventsBatch(x, batchSize).GetAwaiter().GetResult())
   .SelectMany(x => x);
foreach (var carEvent in events)
{
}

Lưu ý .GetAwaiter (). GetResult () chuyển đổi nó thành đồng bộ hóa. DB sẽ chỉ bị tấn công một cách lười biếng khi batchSize của các sự kiện đã được xử lý.


1

Sử dụng Task.WaitAllhoặc Task.WhenAlltùy theo điều kiện nào được chấp thuận.


1
Điều đó cũng không hoạt động. Task.WaitAllđang chặn, không chờ được và sẽ không hoạt động với a Task<T>.
Matt Johnson-Pint

@MattJohnson WhenAll?
LB

Vâng. Đó là nó! Tôi thấy ngớ ngẩn. Cảm ơn!
Matt Johnson-Pint

0

Task.WhenAll nên thực hiện thủ thuật ở đây.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.