Perbedaan antara await dan ContinueWith

119

Adakah yang bisa menjelaskan jika awaitdanContinueWith sama atau tidak dalam contoh berikut. Saya mencoba menggunakan TPL untuk pertama kalinya dan telah membaca semua dokumentasi, tetapi tidak mengerti perbedaannya.

Menunggu :

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

ContinueWith :

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

Apakah yang satu lebih disukai daripada yang lain dalam situasi tertentu?

Harrison
sumber
3
Jika Anda menghapus Waitpanggilan pada contoh kedua maka kedua cuplikan tersebut akan (sebagian besar) setara.
Pelayanan
FYI: getWebPageMetode Anda tidak dapat digunakan di kedua kode. Dalam kode pertama memiliki Task<string>tipe kembali sedangkan di kode kedua memiliki stringtipe kembali. jadi pada dasarnya kode Anda tidak dapat dikompilasi. - jika tepatnya.
Royi Namir

Jawaban:

101

Di kode kedua, Anda secara bersamaan menunggu kelanjutannya selesai. Pada versi pertama, metode akan kembali ke pemanggil segera setelah mencapai awaitekspresi pertama yang belum selesai.

Mereka sangat mirip karena keduanya menjadwalkan kelanjutan, tetapi segera setelah aliran kontrol menjadi sedikit rumit, awaitmengarah ke kode yang jauh lebih sederhana. Selain itu, seperti dicatat oleh Servy dalam komentar, menunggu tugas akan "membuka" agregat pengecualian yang biasanya mengarah pada penanganan kesalahan yang lebih sederhana. awaitPenggunaan juga akan secara implisit menjadwalkan kelanjutan dalam konteks panggilan (kecuali Anda menggunakan ConfigureAwait). Tidak ada yang tidak bisa dilakukan "secara manual", tapi jauh lebih mudah melakukannya await.

Saya sarankan Anda mencoba menerapkan urutan operasi yang sedikit lebih besar dengan keduanya awaitdan Task.ContinueWith- ini bisa menjadi pembuka mata yang nyata.

Jon Skeet
sumber
2
Penanganan kesalahan antara dua potongan juga berbeda; umumnya lebih mudah untuk awaitmengatasi masalah ContinueWithitu.
Pelayanan
@ Servy: Benar, akan menambahkan sesuatu di sekitarnya.
Jon Skeet
1
Penjadwalannya juga sangat berbeda, yaitu, konteks apa yang parseDatadijalankan.
Stephen Cleary
Ketika Anda mengatakan menggunakan await akan secara implisit menjadwalkan kelanjutan dalam konteks panggilan , dapatkah Anda menjelaskan manfaatnya dan apa yang terjadi dalam situasi lain?
Harrison
4
@Harrison: Bayangkan Anda sedang menulis aplikasi WinForms - jika Anda menulis metode asinkron, secara default semua kode dalam metode tersebut akan berjalan di thread UI, karena kelanjutannya akan dijadwalkan di sana. Jika Anda tidak menentukan di mana Anda ingin kelanjutan dijalankan, saya tidak tahu apa defaultnya tetapi bisa dengan mudah berakhir di thread pool thread ... di titik mana Anda tidak dapat mengakses UI, dll. .
Jon Skeet
100

Berikut urutan cuplikan kode yang baru-baru ini saya gunakan untuk menggambarkan perbedaan dan berbagai masalah menggunakan pemecahan asinkron.

Misalkan Anda memiliki beberapa event handler di aplikasi berbasis GUI yang membutuhkan banyak waktu, sehingga Anda ingin membuatnya asynchronous. Inilah logika sinkron yang Anda mulai:

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

LoadNextItem mengembalikan sebuah Tugas, yang pada akhirnya akan menghasilkan beberapa hasil yang ingin Anda periksa. Jika hasil saat ini adalah yang Anda cari, perbarui nilai beberapa penghitung di UI, dan kembali dari metode. Jika tidak, Anda terus memproses lebih banyak item dari LoadNextItem.

Ide pertama untuk versi asynchronous: cukup gunakan lanjutan! Dan mari kita abaikan bagian perulangan untuk saat ini. Maksud saya, apa yang mungkin salah?

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

Hebat, sekarang kami memiliki metode yang tidak memblokir! Itu malah crash. Setiap pembaruan pada kontrol UI harus terjadi pada UI thread, jadi Anda harus memperhitungkannya. Untungnya, ada opsi untuk menentukan bagaimana kelanjutan harus dijadwalkan, dan ada opsi default hanya untuk ini:

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

Hebat, sekarang kami memiliki metode yang tidak macet! Itu gagal secara diam-diam. Kelanjutan adalah tugas yang terpisah, dengan statusnya tidak terikat dengan tugas sebelumnya. Jadi, meskipun LoadNextItem gagal, pemanggil hanya akan melihat tugas yang telah berhasil diselesaikan. Oke, lalu teruskan pengecualiannya, jika ada:

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());

Hebat, sekarang ini benar-benar berfungsi. Untuk satu item. Sekarang, bagaimana dengan perulangan itu. Ternyata, solusi yang setara dengan logika versi sinkron asli akan terlihat seperti ini:

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);
        }});
}

Atau, alih-alih semua hal di atas, Anda dapat menggunakan asinkron untuk melakukan hal yang sama:

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

Itu jauh lebih bagus sekarang, bukan?

pkt
sumber
Terima kasih, penjelasan yang sangat bagus
Elger Mensonides
Ini adalah contoh yang bagus
Royi Namir