Tại sao tôi nên chọn một nhiệm vụ 'đang chờ Nhiệm vụ.When ALL' hơn nhiều lần chờ đợi?


126

Trong trường hợp tôi không quan tâm đến thứ tự hoàn thành nhiệm vụ và chỉ cần tất cả chúng để hoàn thành, tôi vẫn nên sử dụng await Task.WhenAllthay vì nhiều await? ví dụ: DoWork2bên dưới một phương thức ưa thích để DoWork1(và tại sao?):

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Program
    {
        static async Task<string> DoTaskAsync(string name, int timeout)
        {
            var start = DateTime.Now;
            Console.WriteLine("Enter {0}, {1}", name, timeout);
            await Task.Delay(timeout);
            Console.WriteLine("Exit {0}, {1}", name, (DateTime.Now - start).TotalMilliseconds);
            return name;
        }

        static async Task DoWork1()
        {
            var t1 = DoTaskAsync("t1.1", 3000);
            var t2 = DoTaskAsync("t1.2", 2000);
            var t3 = DoTaskAsync("t1.3", 1000);

            await t1; await t2; await t3;

            Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }

        static async Task DoWork2()
        {
            var t1 = DoTaskAsync("t2.1", 3000);
            var t2 = DoTaskAsync("t2.2", 2000);
            var t3 = DoTaskAsync("t2.3", 1000);

            await Task.WhenAll(t1, t2, t3);

            Console.WriteLine("DoWork2 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
        }


        static void Main(string[] args)
        {
            Task.WhenAll(DoWork1(), DoWork2()).Wait();
        }
    }
}

2
Điều gì xảy ra nếu bạn không thực sự biết có bao nhiêu nhiệm vụ bạn cần làm song song? Điều gì nếu bạn có 1000 nhiệm vụ cần phải được chạy? Cái đầu tiên sẽ không dễ đọc lắm await t1; await t2; ....; await tn=> cái thứ hai luôn là lựa chọn tốt nhất trong cả hai trường hợp
cuongle

Nhận xét của bạn có ý nghĩa. Tôi chỉ cố gắng làm rõ điều gì đó cho bản thân mình, liên quan đến một câu hỏi khác mà tôi mới trả lời . Trong trường hợp đó, có 3 nhiệm vụ.
avo

Câu trả lời:


112

Có, sử dụng WhenAllvì nó tuyên truyền tất cả các lỗi cùng một lúc. Với nhiều lần chờ, bạn sẽ mất lỗi nếu một trong những lần chờ trước đó bị ném.

Một sự khác biệt quan trọng khác là WhenAllsẽ chờ tất cả các nhiệm vụ hoàn thành ngay cả khi có lỗi (nhiệm vụ bị lỗi hoặc bị hủy). Chờ đợi theo cách thủ công sẽ gây ra sự đồng thời bất ngờ vì một phần chương trình của bạn muốn chờ đợi sẽ thực sự tiếp tục sớm.

Tôi nghĩ rằng nó cũng làm cho việc đọc mã dễ dàng hơn bởi vì ngữ nghĩa mà bạn muốn được ghi lại trực tiếp trong mã.


9
Vì nó lan truyền tất cả các lỗi cùng một lúc. Không phải awaitlà kết quả của bạn .
Svick

2
Đối với câu hỏi về cách các trường hợp ngoại lệ được quản lý với Nhiệm vụ, bài viết này cung cấp một cái nhìn sâu sắc nhưng nhanh chóng cho lý do đằng sau nó (và nó cũng xảy ra để ghi chú về lợi ích của Khi All Ngược lại với nhiều chờ đợi): blog .msdn.com / b / pfxteam / archive / 2011/09/28 / 10217876.aspx
Oskar Lindberg

5
@OskarLindberg OP đang bắt đầu tất cả các nhiệm vụ trước khi anh ấy chờ đợi nhiệm vụ đầu tiên. Vì vậy, họ chạy đồng thời. Cảm ơn các liên kết.
usr

3
@usr Tôi vẫn tò mò muốn biết khi nào AllAll không làm những việc thông minh như bảo tồn cùng một SyncizationContext, để đẩy lợi ích của nó sang một bên ngoài ngữ nghĩa. Tôi không tìm thấy tài liệu kết luận nào, nhưng nhìn vào IL rõ ràng có các triển khai IAsyncStateMachine khác nhau. Tôi không đọc IL tốt lắm, nhưng ít nhất khi AllAll xuất hiện để tạo mã IL hiệu quả hơn. (Trong mọi trường hợp, thực tế một mình rằng kết quả của Khi All phản ánh trạng thái của tất cả các nhiệm vụ liên quan đến tôi là lý do đủ để thích nó trong hầu hết các trường hợp.)
Oskar Lindberg

17
Một sự khác biệt quan trọng khác là Khi AllAll sẽ đợi tất cả các nhiệm vụ hoàn thành, ngay cả khi, ví dụ, t1 hoặc t2 ném ngoại lệ hoặc bị hủy.
Magnus

28

Sự hiểu biết của tôi là lý do chính để thích Task.WhenAllnhiều awaits là hiệu năng / nhiệm vụ "chasing": DoWork1phương thức làm một cái gì đó như thế này:

  • bắt đầu với một bối cảnh nhất định
  • lưu lại bối cảnh
  • chờ t1
  • khôi phục bối cảnh ban đầu
  • lưu lại bối cảnh
  • chờ t2
  • khôi phục bối cảnh ban đầu
  • lưu lại bối cảnh
  • chờ t3
  • khôi phục bối cảnh ban đầu

Ngược lại, DoWork2làm điều này:

  • bắt đầu với một bối cảnh nhất định
  • lưu lại bối cảnh
  • chờ tất cả t1, t2 và t3
  • khôi phục bối cảnh ban đầu

Cho dù đây là một thỏa thuận đủ lớn cho trường hợp cụ thể của bạn, tất nhiên, là "phụ thuộc vào ngữ cảnh" (tha thứ cho trò chơi chữ).


4
Bạn dường như nghĩ rằng gửi một tin nhắn tot anh bối cảnh đồng bộ hóa là tốn kém. Nó thực sự không. Bạn có một đại biểu được thêm vào hàng đợi, hàng đợi đó sẽ được đọc và đại biểu được thực thi. Chi phí mà nó thêm vào là rất nhỏ. Nó không là gì, nhưng nó cũng không lớn. Chi phí cho bất kỳ hoạt động không đồng bộ nào sẽ giảm chi phí như vậy trong hầu hết các trường hợp.
Phục vụ

Đồng ý, đó chỉ là lý do duy nhất tôi có thể nghĩ đến để thích cái này hơn cái kia. Chà, điều đó cộng với sự tương đồng với Task.WaitTất cả việc chuyển đổi luồng là một chi phí đáng kể hơn.
Marcel Popescu

1
@Servy Như Marcel chỉ ra rằng THỰC SỰ phụ thuộc. Ví dụ, nếu bạn sử dụng await trên tất cả các tác vụ db, và db đó nằm trên cùng một máy như đối tượng asp.net, sẽ có trường hợp bạn chờ đợi một db đạt được trong bộ nhớ trong chỉ mục, rẻ hơn hơn công tắc đồng bộ hóa và xáo trộn luồng. Có thể có một chiến thắng chung đáng kể với Khi All () trong loại kịch bản đó, vì vậy ... nó thực sự phụ thuộc.
Chris Moschini

3
@ChrisMoschini Không có cách nào mà truy vấn DB, ngay cả khi nó đánh một DB ngồi trên cùng một máy với máy chủ, sẽ nhanh hơn so với việc thêm một vài đại biểu vào một bơm thông báo. Truy vấn trong bộ nhớ đó vẫn gần như chắc chắn sẽ chậm hơn khá nhiều.
Phục vụ

Cũng lưu ý rằng nếu t1 chậm hơn và t2 và t3 nhanh hơn - thì những thứ khác đang chờ trả lại ngay lập tức.
David Refaeli

18

Một phương pháp không đồng bộ được thực hiện như một máy trạng thái. Có thể viết các phương thức để chúng không được biên dịch thành các máy trạng thái, điều này thường được gọi là phương thức async theo dõi nhanh. Đây có thể được thực hiện như vậy:

public Task DoSomethingAsync()
{
    return DoSomethingElseAsync();
}

Khi sử dụng Task.WhenAll, có thể duy trì mã theo dõi nhanh này trong khi vẫn đảm bảo người gọi có thể đợi tất cả các tác vụ được hoàn thành, ví dụ:

public Task DoSomethingAsync()
{
    var t1 = DoTaskAsync("t2.1", 3000);
    var t2 = DoTaskAsync("t2.2", 2000);
    var t3 = DoTaskAsync("t2.3", 1000);

    return Task.WhenAll(t1, t2, t3);
}

7

(Tuyên bố miễn trừ trách nhiệm: Câu trả lời này được lấy / lấy cảm hứng từ khóa học TPL Async của Ian Griffiths về Pluralsight )

Một lý do khác để thích Khi AllAll là xử lý Ngoại lệ.

Giả sử bạn đã có một khối thử bắt trên các phương thức DoWork của mình và giả sử họ đang gọi các phương thức DoTask khác nhau:

static async Task DoWork1() // modified with try-catch
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        await t1; await t2; await t3;

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t1.Result, t2.Result, t3.Result));
    }
    catch (Exception x)
    {
        // ...
    }

}

Trong trường hợp này, nếu cả 3 nhiệm vụ đưa ra ngoại lệ, chỉ có nhiệm vụ đầu tiên sẽ bị bắt. Bất kỳ ngoại lệ sau này sẽ bị mất. Tức là nếu t2 và t3 ném ngoại lệ, chỉ t2 sẽ bị bắt; vv Các ngoại lệ nhiệm vụ tiếp theo sẽ không được quan sát.

Trong trường hợp như khi AllAll - nếu có hoặc tất cả các tác vụ bị lỗi, tác vụ kết quả sẽ chứa tất cả các ngoại lệ. Từ khóa await vẫn luôn ném lại ngoại lệ đầu tiên. Vì vậy, các ngoại lệ khác vẫn không được quan sát một cách hiệu quả. Một cách để khắc phục điều này là thêm phần tiếp tục trống sau nhiệm vụ Khi Tất cả và đặt sự chờ đợi ở đó. Theo cách này, nếu tác vụ thất bại, thuộc tính kết quả sẽ đưa ra Ngoại lệ tổng hợp đầy đủ:

static async Task DoWork2() //modified to catch all exceptions
{
    try
    {
        var t1 = DoTask1Async("t1.1", 3000);
        var t2 = DoTask2Async("t1.2", 2000);
        var t3 = DoTask3Async("t1.3", 1000);

        var t = Task.WhenAll(t1, t2, t3);
        await t.ContinueWith(x => { });

        Console.WriteLine("DoWork1 results: {0}", String.Join(", ", t.Result[0], t.Result[1], t.Result[2]));
    }
    catch (Exception x)
    {
        // ...
    }
}

6

Các câu trả lời khác cho câu hỏi này cung cấp lý do kỹ thuật tại sao await Task.WhenAll(t1, t2, t3);được ưa thích. Câu trả lời này sẽ nhằm mục đích xem xét nó từ một khía cạnh nhẹ nhàng hơn (mà @usr ám chỉ) trong khi vẫn đi đến kết luận tương tự.

await Task.WhenAll(t1, t2, t3); là một cách tiếp cận chức năng hơn, vì nó tuyên bố ý định và là nguyên tử.

Với await t1; await t2; await t3;, không có gì ngăn cản đồng đội (hoặc thậm chí là bản thân tương lai của bạn!) Thêm mã giữa các cá nhânawait câu lệnh . Chắc chắn, bạn đã nén nó thành một dòng để thực hiện điều đó, nhưng điều đó không giải quyết được vấn đề. Bên cạnh đó, hình thức nói chung là xấu trong cài đặt nhóm để bao gồm nhiều câu lệnh trên một dòng mã nhất định, vì nó có thể làm cho tệp nguồn của mắt người khó quét hơn.

Nói một cách đơn giản, await Task.WhenAll(t1, t2, t3);có thể duy trì nhiều hơn, vì nó truyền đạt ý định của bạn rõ ràng hơn và ít bị tổn thương hơn với các lỗi đặc biệt có thể phát sinh từ các bản cập nhật có ý nghĩa đối với mã hoặc thậm chí chỉ hợp nhất đã sai.

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.