Async đang chờ trong linq chọn


180

Tôi cần sửa đổi một chương trình hiện có và nó chứa mã sau đây:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Nhưng điều này có vẻ rất kỳ lạ đối với tôi, trước hết là việc sử dụng asyncawaittrong việc lựa chọn. Theo câu trả lời này của Stephen Cleary tôi sẽ có thể bỏ những thứ đó.

Sau đó, thứ hai Select chọn kết quả. Điều này không có nghĩa là nhiệm vụ hoàn toàn không đồng bộ và được thực hiện đồng bộ (rất nhiều nỗ lực không có gì), hoặc nhiệm vụ sẽ được thực hiện không đồng bộ và khi hoàn thành phần còn lại của truy vấn?

Tôi có nên viết đoạn mã trên như sau theo một câu trả lời khác của Stephen Cleary :

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

và nó hoàn toàn giống như thế này?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Trong khi tôi đang làm việc trong dự án này, tôi muốn thay đổi mẫu mã đầu tiên nhưng tôi không quá quan tâm đến việc thay đổi mã async (làm việc một cách tự nhiên). Có lẽ tôi chỉ lo lắng không có gì và cả 3 mẫu mã đều làm chính xác như vậy?

ProcessEventsAsync trông như thế này:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}

Loại trả về của ProceesEventAsync là gì?
tede24

@ tede24 Đó là Task<InputResult>với InputResultlà một lớp tùy chỉnh.
Alexander Derck

Theo ý kiến ​​của tôi, phiên bản của bạn dễ đọc hơn nhiều. Tuy nhiên, bạn đã quên Selectkết quả từ các nhiệm vụ trước của bạn Where.
Tối đa

Và InputResult có thuộc tính Kết quả phải không?
tede24

@ tede24 Kết quả là tài sản của nhiệm vụ không phải lớp tôi. Và @Max sự chờ đợi sẽ đảm bảo tôi nhận được kết quả mà không cần truy cập vào Resulttài sản của nhiệm vụ
Alexander Derck

Câu trả lời:


184
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

Nhưng điều này có vẻ rất kỳ lạ đối với tôi, trước hết là việc sử dụng async và chờ đợi trong phần chọn. Theo câu trả lời này của Stephen Cleary tôi sẽ có thể bỏ những thứ đó.

Cuộc gọi đến Selectlà hợp lệ. Hai dòng này về cơ bản là giống hệt nhau:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(Có một sự khác biệt nhỏ về cách một ngoại lệ đồng bộ sẽ được ném ra ProcessEventAsync, nhưng trong bối cảnh của mã này, nó hoàn toàn không thành vấn đề.)

Sau đó chọn thứ hai Chọn kết quả. Điều này không có nghĩa là nhiệm vụ hoàn toàn không đồng bộ và được thực hiện đồng bộ (rất nhiều nỗ lực không có gì), hoặc nhiệm vụ sẽ được thực hiện không đồng bộ và khi hoàn thành phần còn lại của truy vấn?

Nó có nghĩa là truy vấn đang chặn. Vì vậy, nó không thực sự không đồng bộ.

Phá vỡ nó:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

đầu tiên sẽ bắt đầu một hoạt động không đồng bộ cho mỗi sự kiện. Sau đó, dòng này:

                   .Select(t => t.Result)

sẽ đợi các hoạt động đó hoàn thành một lần (đầu tiên nó chờ hoạt động của sự kiện đầu tiên, sau đó tiếp theo, sau đó tiếp theo, v.v.).

Đây là phần tôi không quan tâm, vì nó chặn và cũng sẽ bao gồm mọi trường hợp ngoại lệ AggregateException.

và nó hoàn toàn giống như thế này?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Vâng, hai ví dụ này là tương đương. Cả hai đều bắt đầu tất cả các hoạt động không đồng bộ ( events.Select(...)), sau đó chờ không đồng bộ cho tất cả các hoạt động hoàn thành theo bất kỳ thứ tự nào ( await Task.WhenAll(...)), sau đó tiến hành phần còn lại của công việc ( Where...).

Cả hai ví dụ này khác với mã gốc. Mã ban đầu đang chặn và sẽ bao bọc các ngoại lệ AggregateException.


Chúc mừng đã xóa nó lên! Vì vậy, thay vì các ngoại lệ được gói gọn trong một AggregateExceptiontôi sẽ nhận được nhiều ngoại lệ riêng biệt trong mã thứ hai?
Alexander Derck

1
@AlexanderDerck: Không, trong cả mã cũ và mã mới, chỉ có ngoại lệ đầu tiên được nêu ra. Nhưng với Resultnó sẽ được bọc trong AggregateException.
Stephen Cleary

Tôi đang gặp bế tắc trong Trình điều khiển ASP.NET MVC của mình bằng mã này. Tôi đã giải quyết nó bằng cách sử dụng tác vụ. Tôi không có cảm giác tốt về nó. Tuy nhiên, nó đã hoàn thành đúng khi chạy vào thử nghiệm async xUnit. Chuyện gì đang xảy ra vậy?
SuperJMN

2
@SuperJMN: Thay thế stuff.Select(x => x.Result);bằngawait Task.WhenAll(stuff)
Stephen Cleary

1
@DanielS: Về cơ bản chúng giống nhau. Có một số khác biệt như máy trạng thái, nắm bắt bối cảnh, hành vi của các ngoại lệ đồng bộ. Thêm thông tin tại blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary

25

Mã hiện tại đang hoạt động, nhưng đang chặn luồng.

.Select(async ev => await ProcessEventAsync(ev))

tạo một tác vụ mới cho mọi sự kiện, nhưng

.Select(t => t.Result)

chặn chuỗi chờ cho mỗi nhiệm vụ mới kết thúc.

Mặt khác, mã của bạn tạo ra kết quả tương tự nhưng vẫn không đồng bộ.

Chỉ cần một nhận xét về mã đầu tiên của bạn. Đường thẳng này

var tasks = await Task.WhenAll(events...

sẽ tạo ra một Nhiệm vụ duy nhất để biến phải được đặt tên theo số ít.

Cuối cùng, mã cuối cùng của bạn làm tương tự nhưng ngắn gọn hơn

Để tham khảo: Nhiệm vụ.Wait / Nhiệm vụ.When ALL


Vì vậy, khối mã đầu tiên trong thực tế được thực hiện đồng bộ?
Alexander Derck

1
Có, bởi vì việc truy cập Kết quả tạo ra Chờ mà chặn chuỗi. Mặt khác Khi sản xuất một Nhiệm vụ mới, bạn có thể chờ đợi.
tede24

1
Quay trở lại câu hỏi này và xem xét nhận xét của bạn về tên của tasksbiến, bạn hoàn toàn đúng. Lựa chọn khủng khiếp, chúng thậm chí không phải là nhiệm vụ khi chúng được chờ đợi ngay lập tức. Mặc dù vậy, tôi sẽ chỉ để lại câu hỏi
Alexander Derck

13

Với các phương thức hiện có trong Linq, nó trông khá xấu xí:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

Hy vọng các phiên bản .NET tiếp theo sẽ xuất hiện với công cụ thanh lịch hơn để xử lý các bộ sưu tập nhiệm vụ và nhiệm vụ của các bộ sưu tập.


12

Tôi đã sử dụng mã này:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

như thế này:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

5
Điều này chỉ gói
gọn

Thay thế là var result = await Task.When ALL (sourceEnumerable.Select (async s => await someFactor (s, other params)). Nó cũng hoạt động, nhưng nó không phải là LINQy
Siderite Zackwehdex

Không nên Func<TSource, Task<TResult>> methodchứa mã other paramsđược đề cập trên bit thứ hai?
matramos

2
Các tham số bổ sung là bên ngoài, tùy thuộc vào chức năng mà tôi muốn thực thi, chúng không liên quan trong ngữ cảnh của phương thức mở rộng.
Siderite Zackwehdex

4
Đó là một phương pháp mở rộng đáng yêu. Không chắc chắn lý do tại sao nó được coi là "tối nghĩa hơn" - nó tương tự về mặt ngữ nghĩa với đồng bộ Select(), do đó, là một kiểu thả thanh lịch.
nullPainter

10

Tôi thích điều này như một phương pháp mở rộng:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

Vì vậy, nó có thể được sử dụng với phương pháp xích:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()

1
Bạn không nên gọi phương thức Waitkhi nó không thực sự chờ đợi. Nó tạo ra một nhiệm vụ hoàn thành khi tất cả các nhiệm vụ hoàn thành. Gọi nó WhenAll, giống như Taskphương pháp mà nó mô phỏng. Phương pháp này cũng vô nghĩa async. Chỉ cần gọi WhenAllvà được thực hiện với nó.
Phục vụ

Theo ý kiến ​​của tôi, một chút về trình bao bọc vô dụng khi nó chỉ gọi phương thức ban đầu
Alexander Derck

@Servy điểm công bằng, nhưng tôi không đặc biệt thích bất kỳ tùy chọn tên nào. Khi All làm cho nó nghe như một sự kiện mà nó không hoàn toàn.
Daryl

3
@AlexanderDerck lợi thế là bạn có thể sử dụng nó trong phương thức xích.
Daryl

1
@Daryl vì WhenAlltrả về một danh sách được đánh giá (nó không được đánh giá một cách lười biếng), nên có thể đưa ra một đối số để sử dụng Task<T[]>kiểu trả về để biểu thị điều đó. Khi được chờ đợi, điều này vẫn sẽ có thể sử dụng Linq, nhưng cũng thông báo rằng nó không lười biếng.
JAD
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.