Câu hỏi Tại sao tôi nên chọn đơn 'chờ đợi Task.WhenAll' trên nhiều đang chờ đợi?


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.WhenAll thay vì nhiều await? Ví dụ: là DoWord2 bên dưới một phương pháp được ưu tiên 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();
        }
    }
}

76
2017-08-19 09:56


gốc


Điều gì xảy ra nếu bạn thực sự không biết có bao nhiêu công việc bạn cần phải làm song song? Điều gì nếu bạn có 1000 nhiệm vụ cần phải được chạy? Người đầu tiên sẽ không thể đọc được nhiều await t1; await t2; ....; await tn => thứ hai luôn là lựa chọn tốt nhất trong cả hai trường hợp - Cuong Le
Bình luận của bạn có ý nghĩa. Tôi chỉ đang cố gắng làm sáng tỏ điều gì đó cho bản thân mình, liên quan đến một câu hỏi khác đã trả lời. Trong trường hợp đó, có 3 nhiệm vụ. - avo


Các câu trả lời:


Có, sử dụng WhenAll bởi vì nó lan truyền tất cả các lỗi cùng một lúc. Với nhiều chờ đợi bạn sẽ mất các lỗi nếu một trong những trước đó đang chờ ném.

Một điểm khác biệt quan trọng là WhenAll sẽ đợi tất cả các nhiệm vụ để hoàn thành. Một chuỗi await sẽ hủy bỏ đang chờ đợi tại ngoại lệ đầu tiên nhưng việc thực hiện các nhiệm vụ không được chờ đợi vẫn tiếp tục. Điều này gây ra đồng thời bất ngờ.

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ã.


73
2017-08-19 10:02



"Bởi vì nó tuyên truyền tất cả các lỗi cùng một lúc" Không phải nếu bạn await kết quả của nó. - svick
Đối với câu hỏi về cách ngoại lệ được quản lý với Task, bài viết này cung cấp một cái nhìn sâu sắc nhưng tốt cho lý do đằng sau nó (và nó chỉ xảy ra cũng làm cho một lưu ý đi qua về lợi ích của WhenAll trái ngược với nhiều đang chờ): blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx - Oskar Lindberg
@OskarLindberg OP đang bắt đầu tất cả các nhiệm vụ trước khi anh ấy đang chờ 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
@usr Tôi đã tò mò vẫn còn biết nếu WhenAll không làm những việc thông minh như bảo tồn cùng một SynchronizationContext, để đẩy mạnh hơn nữa lợi ích của nó ngoài các ngữ nghĩa. Tôi không tìm thấy tài liệu nào có kết luận, nhưng nhìn vào IL, có những cách triển khai khác nhau rõ ràng của IAsyncStateMachine khi chơi. Tôi không đọc IL tất cả những điều đó, nhưng WhenAll ít nhất xuất hiện để tạo ra 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 WhenAll 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
Một điểm khác biệt quan trọng là WhenAll sẽ đợi tất cả các tác 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


Sự hiểu biết của tôi là lý do chính để thích Task.WhenAll đến nhiều awaits là hiệu suất / nhiệm vụ "khuấy": DoWork1 phương pháp làm một cái gì đó như thế này:

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

Ngược lại, DoWork2 thực hiện điều này:

  • bắt đầu với một ngữ cảnh cụ thể
  • lưu ngữ cảnh
  • chờ tất cả t1, t2 và t3
  • khôi phục ngữ 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, "phụ thuộc vào ngữ cảnh" (tha thứ cho sự chơi chữ).


19
2017-11-27 16:39



Dường như bạn nghĩ rằng việc gửi một tin nhắn thì bối cảnh đồng bộ hóa của anh ta rất đắt. 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à ủy nhiệm được thực hiện. Các chi phí mà điều này cho biết thêm là trung thực rất nhỏ. Nó không phải là gì, nhưng nó cũng không lớn. Các chi phí của bất kỳ hoạt động async sẽ lùn như trên không trong hầu như tất cả các trường hợp. - Servy
Đồng ý, đó chỉ là lý do duy nhất tôi có thể nghĩ là thích cái này hơn cái kia. Vâng, điều đó cộng với sự giống nhau với Task.WaitAll trong đó chuyển đổi luồng là chi phí đáng kể hơn. - Marcel Popescu
@Servy Như Marcel chỉ ra rằng REALLY phụ thuộc. Nếu bạn sử dụng chờ đợi trên tất cả các nhiệm vụ db như là một vấn đề của nguyên tắc ví dụ, và rằng db ngồi trên cùng một máy như dụ asp.net, có những trường hợp bạn sẽ chờ đợi một hit db đó là trong bộ nhớ trong chỉ mục, rẻ hơn so với chuyển đổi đồng bộ hóa đó và trộn ngẫu nhiên. Có thể có một chiến thắng tổng thể đáng kể với WhenAll () trong loại kịch bản đó, vì vậy ... nó thực sự phụ thuộc. - Chris Moschini
@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 như máy chủ, sẽ nhanh hơn chi phí của việc thêm một vài đại biểu vào một máy bơm tin nhắn. Truy vấn trong bộ nhớ đó hầu như chắc chắn sẽ chậm hơn rất nhiều. - Servy
Cũng lưu ý rằng nếu t1 chậm hơn và t2 và t3 nhanh hơn - thì người kia đang chờ trở lại ngay lập tức. - David Refaeli


Một phương thức 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à một 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 đang 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);
}

11
2017-12-24 16:25





Các câu trả lời khác cho câu hỏi này đưa ra các 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 nhìn vào nó từ một mặt nhẹ nhàng hơn (mà @usr ám chỉ) trong khi vẫn đi đến cùng một kết luận.

await Task.WhenAll(t1, t2, t3); là một phương pháp 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 một đồng đội (hoặc thậm chí cả bản thân tương lai của bạn!) từ việc thêm mã giữa cá nhân await các câu lệnh. Chắc chắn, bạn đã nén nó thành một dòng về cơ bản thực hiện điều đó, nhưng điều đó không giải quyết được vấn đề. Bên cạnh đó, nó thường là hình thức xấu trong một nhóm thiết lập để bao gồm nhiều báo cáo trên một dòng mã nhất định, vì nó có thể làm cho tập tin nguồn khó khăn hơn cho mắt người để quét.

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


0
2018-01-06 00:11





(Disclaimer: 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 WhenAll là xử lý ngoại lệ.

Giả sử bạn có một khối try-catch trên các phương thức DoWork và giả sử chúng đ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 tất cả 3 nhiệm vụ ném ngoại lệ, chỉ có một nhiệm vụ đầu tiên sẽ bị bắt. Bất kỳ ngoại lệ nào sau này cũng sẽ bị mất. I E. nếu t2 và t3 ném ngoại lệ, chỉ t2 sẽ bị bắt; vv Các trường hợp ngoại lệ nhiệm vụ tiếp theo sẽ không được giám sát.

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

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)
    {
        // ...
    }
}

0
2018-05-05 15:33