Sự khác biệt giữa await và ContinueWith


119

Ai đó có thể giải thích nếu awaitContinueWithđồng nghĩa hay không trong ví dụ sau. Tôi đang cố gắng sử dụng TPL lần đầu tiên và đã đọc tất cả các tài liệu, nhưng không hiểu sự khác biệt.

Chờ đợi :

String webText = await getWebPage(uri);
await parseData(webText);

Tiếp tụcVới :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

Cái này có được ưu tiên hơn cái kia trong những tình huống cụ thể không?


3
Nếu bạn loại bỏ Waitcuộc gọi trong ví dụ thứ hai thì hai đoạn mã sẽ (hầu hết) tương đương.
Servy


FYI: getWebPageKhông thể sử dụng phương pháp của bạn trong cả hai mã. Trong đoạn mã đầu tiên, nó có Task<string>kiểu trả về trong khi ở đoạn mã thứ hai, nó có stringkiểu trả về. vì vậy về cơ bản mã của bạn không biên dịch. - nếu chính xác.
Royi Namir

Câu trả lời:


101

Trong mã thứ hai, bạn đang chờ đồng bộ hóa quá trình tiếp tục hoàn tất. Trong phiên bản đầu tiên, phương thức sẽ trở lại trình gọi ngay sau khi nó chạm vào awaitbiểu thức đầu tiên chưa được hoàn thành.

Chúng rất giống nhau ở chỗ cả hai đều lên lịch tiếp tục, nhưng ngay khi luồng điều khiển thậm chí còn hơi phức tạp, awaitdẫn đến mã đơn giản hơn nhiều . Ngoài ra, như Servy đã lưu ý trong các bình luận, việc chờ đợi một nhiệm vụ sẽ "mở" các ngoại lệ tổng hợp thường dẫn đến việc xử lý lỗi đơn giản hơn. Ngoài ra, việc sử dụng awaitsẽ lên lịch ngầm cho việc tiếp tục trong ngữ cảnh gọi (trừ khi bạn sử dụng ConfigureAwait). Không có gì là không thể được thực hiện "thủ công", nhưng nó dễ dàng hơn rất nhiều await.

Tôi khuyên bạn nên thử thực hiện một chuỗi hoạt động lớn hơn một chút với cả hai awaitTask.ContinueWith- nó có thể là một thứ mở mang tầm mắt thực sự.


2
Việc xử lý lỗi giữa hai đoạn mã cũng khác nhau; nó thường dễ dàng hơn để làm việc với awaithơn ContinueWithvề vấn đề đó.
Servy

@Servy: Đúng, sẽ thêm một số thứ xung quanh điều đó.
Jon Skeet

1
Bố trí thời vụ cũng khá khác nhau, ví dụ, những gì bối cảnh parseDatathực thi trong.
Stephen Cleary

Khi bạn nói việc sử dụng await sẽ lên lịch ngầm cho việc tiếp tục trong ngữ cảnh gọi , bạn có thể giải thích lợi ích của điều đó và điều gì xảy ra trong tình huống khác không?
Harrison

4
@Harrison: Hãy tưởng tượng bạn đang viết một ứng dụng WinForms - nếu bạn viết một phương thức không đồng bộ, theo mặc định, tất cả mã trong phương thức này sẽ chạy trong chuỗi giao diện người dùng, vì việc tiếp tục sẽ được lên lịch ở đó. Nếu bạn không chỉ định nơi bạn muốn phần tiếp tục chạy, tôi không biết mặc định là gì nhưng nó có thể dễ dàng kết thúc chạy trên một chuỗi nhóm luồng ... tại thời điểm đó bạn không thể truy cập vào giao diện người dùng, v.v. .
Jon Skeet

100

Dưới đây là chuỗi các đoạn mã mà tôi đã sử dụng gần đây để minh họa sự khác biệt và các vấn đề khác nhau khi sử dụng giải pháp không đồng bộ.

Giả sử bạn có một số trình xử lý sự kiện trong ứng dụng dựa trên GUI của mình, mất rất nhiều thời gian và vì vậy bạn muốn làm cho nó không đồng bộ. Đây là logic đồng bộ mà bạn bắt đầu:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem trả về một Tác vụ, cuối cùng sẽ tạo ra một số kết quả mà bạn muốn kiểm tra. Nếu kết quả hiện tại là kết quả bạn đang tìm kiếm, bạn cập nhật giá trị của một số bộ đếm trên giao diện người dùng và trả về từ phương thức. Nếu không, bạn tiếp tục xử lý các mục khác từ LoadNextItem.

Ý tưởng đầu tiên cho phiên bản không đồng bộ: chỉ cần sử dụng liên tục! Và chúng ta hãy bỏ qua phần lặp lại trong lúc này. Ý tôi là, điều gì có thể xảy ra?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Tuyệt vời, bây giờ chúng ta có một phương pháp không chặn! Thay vào đó, nó bị treo. Mọi cập nhật đối với các điều khiển giao diện người dùng sẽ xảy ra trên chuỗi giao diện người dùng, vì vậy bạn sẽ cần tính đến điều đó. Rất may, có một tùy chọn để chỉ định cách lập lịch trình liên tục và có một tùy chọn mặc định cho điều này:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Tuyệt vời, bây giờ chúng tôi có một phương pháp không sụp đổ! Thay vào đó, nó thất bại một cách âm thầm. Bản thân các nhiệm vụ tiếp tục là các nhiệm vụ riêng biệt, với trạng thái của chúng không gắn với trạng thái của nhiệm vụ trước đó. Vì vậy, ngay cả khi LoadNextItem lỗi, người gọi sẽ chỉ thấy một tác vụ đã hoàn thành thành công. Được rồi, chỉ cần chuyển ngoại lệ, nếu có:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Tuyệt vời, bây giờ điều này thực sự hoạt động. Đối với một mặt hàng duy nhất. Bây giờ, làm thế nào về vòng lặp đó. Hóa ra, một giải pháp tương đương với logic của phiên bản đồng bộ ban đầu sẽ trông giống như sau:

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

Hoặc, thay vì tất cả những điều trên, bạn có thể sử dụng async để làm điều tương tự:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

Bây giờ đẹp hơn rất nhiều, phải không?


Cảm ơn, lời giải thích thực sự hay
Elger Mensonides

Đây là một ví dụ tuyệt vời
Royi Namir
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.