Apa tujuan dari "menunggu kembali" di C #?

251

Apakah ada setiap skenario di mana menulis metode seperti ini:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

alih-alih ini:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

akan masuk akal?

Mengapa menggunakan return awaitkonstruk ketika Anda dapat langsung kembali Task<T>dari DoAnotherThingAsync()doa batin ?

Saya melihat kode dengan return awaitdi banyak tempat, saya pikir saya mungkin melewatkan sesuatu. Tapi sejauh yang saya mengerti, tidak menggunakan kata kunci async / menunggu dalam hal ini dan langsung mengembalikan Tugas akan setara secara fungsional. Mengapa menambahkan overhead tambahan dari awaitlapisan tambahan ?

TX_
sumber
2
Saya pikir satu-satunya alasan mengapa Anda melihat ini adalah karena orang belajar dengan meniru dan umumnya (jika mereka tidak perlu) mereka menggunakan solusi paling sederhana yang dapat mereka temukan. Jadi orang-orang melihat kode itu, menggunakan kode itu, mereka melihatnya bekerja dan mulai sekarang, bagi mereka, itulah cara yang tepat untuk melakukannya ... Tidak ada gunanya menunggu dalam kasus itu
Fabio Marcolini
7
Setidaknya ada satu perbedaan penting: perkecualian perkecualian .
noseratio
1
Saya juga tidak mengerti, tidak bisa memahami seluruh konsep ini sama sekali, tidak masuk akal. Dari apa yang saya pelajari jika suatu metode memiliki tipe kembali, ITU HARUS memiliki kata kunci kembali, bukankah itu aturan bahasa C #?
monstro
@monstro pertanyaan OP memang memiliki pernyataan pengembalian?
David Klempfner

Jawaban:

189

Ada satu kasus licik ketika returndalam metode normal dan return awaitdalam asyncmetode berperilaku berbeda: ketika dikombinasikan dengan using(atau, lebih umum, apa pun return awaitdalam satu tryblok).

Pertimbangkan dua versi metode ini:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Metode pertama akan Dispose()dengan Fooobjek segera setelah DoAnotherThingAsync()kembali metode, yang kemungkinan jauh sebelum benar-benar selesai. Ini berarti versi pertama mungkin buggy (karena Foodibuang terlalu cepat), sedangkan versi kedua akan berfungsi dengan baik.

svick
sumber
4
Untuk kelengkapan, dalam kasus pertama Anda harus kembalifoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord
7
@ ghord Itu tidak akan berhasil, Dispose()kembali void. Anda akan membutuhkan sesuatu seperti return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Tapi saya tidak tahu mengapa Anda melakukan itu ketika Anda dapat menggunakan opsi kedua.
svick
1
@ svick Anda benar, seharusnya lebih sesuai { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Kasus penggunaannya cukup sederhana: jika Anda menggunakan .NET 4.0 (seperti kebanyakan), Anda masih dapat menulis kode async dengan cara ini yang akan berfungsi dengan baik dipanggil dari 4,5 aplikasi.
ghord
2
@ ghord Jika Anda menggunakan .Net 4.0 dan Anda ingin menulis kode asinkron, Anda mungkin harus menggunakan Microsoft.Bcl.Async . Dan kode Anda dibuang Foohanya setelah Taskselesai kembali , yang saya tidak suka, karena tidak perlu memperkenalkan concurrency.
svick
1
@ svick Kode Anda menunggu hingga tugas selesai juga. Juga, Microsoft.Bcl.Async tidak dapat digunakan untuk saya karena ketergantungan pada KB2468871 dan konflik saat menggunakan .NET 4.0 async codebase dengan kode async 4,5 yang tepat.
ghord
93

Jika Anda tidak perlu async(yaitu, Anda dapat mengembalikan Tasklangsung), maka jangan gunakan async.

Ada beberapa situasi di mana return awaitberguna, seperti jika Anda memiliki dua operasi asinkron untuk dilakukan:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Untuk lebih lanjut tentang asynckinerja, lihat artikel MSDN dan video Stephen Toub tentang topik ini.

Pembaruan: Saya telah menulis posting blog yang lebih detail.

Stephen Cleary
sumber
13
Bisakah Anda menambahkan penjelasan mengapa awaitini berguna dalam kasus kedua? Kenapa tidak return SecondAwait(intermediate);?
Matt Smith
2
Saya memiliki pertanyaan yang sama dengan Matt, tidakkah akan return SecondAwait(intermediate);mencapai tujuan dalam kasus itu juga? Saya pikir return awaitjuga berlebihan di sini ...
TX_30
23
@ MatSmith Itu tidak bisa dikompilasi. Jika Anda ingin menggunakan awaitdi baris pertama, Anda harus menggunakannya di baris kedua juga.
svick
2
@cateyes Saya tidak yakin apa artinya "overhead diperkenalkan dengan cara menyejajarkan mereka", tetapi asyncversi itu akan menggunakan lebih sedikit sumber daya (utas) daripada versi sinkron Anda.
svick
4
@ TomLint Ini benar-benar tidak dikompilasi. Dengan asumsi tipe kembalinya SecondAwaitadalah `string, pesan galatnya adalah:" CS4016: Karena ini adalah metode async, ekspresi kembalian harus dari tipe 'string' daripada 'Task <string>' ".
svick
23

Satu-satunya alasan Anda ingin melakukannya adalah jika ada yang lain awaitdalam kode sebelumnya, atau jika Anda dengan cara tertentu memanipulasi hasilnya sebelum mengembalikannya. Cara lain yang mungkin terjadi adalah melalui try/catchperubahan yang mengubah cara pengecualian ditangani. Jika Anda tidak melakukan semua itu maka Anda benar, tidak ada alasan untuk menambahkan biaya tambahan untuk membuat metode async.

Melayani
sumber
4
Seperti jawaban Stephen, saya tidak mengerti mengapa return awaitperlu (alih-alih hanya mengembalikan tugas pemanggilan anak) bahkan jika ada beberapa yang lain menunggu dalam kode sebelumnya . Bisakah Anda memberikan penjelasan?
TX_30
10
@ TX_ Jika Anda ingin menghapus asynclalu bagaimana Anda menunggu tugas pertama? Anda perlu menandai metode seolah- asyncolah Anda ingin menggunakan apa pun yang menunggu. Jika metode ini ditandai sebagai asyncdan Anda memiliki awaitkode sebelumnya, maka Anda perlu awaitoperasi async kedua agar jenisnya sesuai. Jika Anda baru saja dihapus awaitmaka itu tidak akan dikompilasi karena nilai pengembalian tidak akan menjadi tipe yang tepat. Karena metode ini asynchasilnya selalu dibungkus dengan tugas.
Servy
11
@Noseratio Coba keduanya. Kompilasi pertama. Yang kedua tidak. Pesan kesalahan akan memberi tahu Anda masalahnya. Anda tidak akan mengembalikan jenis yang tepat. Ketika dalam suatu asyncmetode Anda tidak mengembalikan tugas, Anda mengembalikan hasil tugas yang kemudian akan dibungkus.
Servy
3
@Servy, tentu saja - Anda benar. Dalam kasus yang terakhir kita akan kembali Task<Type>secara eksplisit, sedangkan asyncperintah untuk kembali Type(yang akan diubah oleh kompiler itu sendiri Task<Type>).
noseratio
3
@Itsik Ya tentu saja, asyncini hanya gula sintaksis untuk melanjutkan secara eksplisit. Anda tidak perlu async melakukan apa pun, tetapi ketika melakukan hampir semua operasi asinkron non-sepele itu secara dramatis lebih mudah untuk dikerjakan. Misalnya, kode yang Anda berikan sebenarnya tidak menyebarkan kesalahan seperti yang Anda inginkan, dan melakukannya dengan benar dalam situasi yang lebih kompleks mulai menjadi jauh lebih sulit. Meskipun Anda tidak pernah perlu async , situasi yang saya jelaskan adalah di mana itu menambah nilai untuk menggunakannya.
Servy
17

Kasus lain yang mungkin perlu Anda tunggu hasilnya adalah yang ini:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

Dalam hal ini, GetIFooAsync()harus menunggu hasil GetFooAsynckarena jenis Tberbeda antara kedua metode dan Task<Foo>tidak langsung ditugaskan Task<IFoo>. Tapi jika Anda menunggu hasilnya, itu hanya menjadi Fooyang adalah langsung dialihkan ke IFoo. Kemudian metode async hanya mengemas kembali hasilnya di dalam Task<IFoo>dan pergi Anda pergi.

Andrew Arnott
sumber
1
Setuju, ini benar-benar menjengkelkan - saya percaya penyebab yang mendasarinya adalah yang Task<>tidak berubah-ubah.
StuartLC
7

Membuat metode "thunk" async yang dinyatakan sederhana membuat mesin status async dalam memori sedangkan yang non-async tidak. Meskipun hal itu sering dapat mengarahkan orang untuk menggunakan versi non-async karena lebih efisien (yang benar) juga berarti bahwa jika terjadi hang, Anda tidak memiliki bukti bahwa metode tersebut terlibat dalam "tumpukan pengembalian / kelanjutan" yang terkadang membuatnya lebih sulit untuk memahami hang.

Jadi ya, ketika perf tidak kritis (dan biasanya tidak) saya akan melempar async pada semua metode thunk sehingga saya memiliki mesin negara async untuk membantu saya mendiagnosis hang nanti, dan juga untuk membantu memastikan bahwa jika mereka Metode thunk pernah berkembang seiring waktu, mereka pasti akan mengembalikan tugas yang salah alih-alih melempar.

Andrew Arnott
sumber
6

Jika Anda tidak akan menggunakan pengembalian menunggu, Anda dapat merusak jejak tumpukan Anda saat debugging atau ketika itu dicetak dalam log pada pengecualian.

Ketika Anda mengembalikan tugas, metode memenuhi tujuannya dan itu keluar dari tumpukan panggilan. Saat Anda menggunakanreturn await Anda meninggalkannya di tumpukan panggilan.

Sebagai contoh:

Tumpukan panggilan saat menggunakan menunggu: A menunggu tugas dari B => B menunggu tugas dari C

Tumpukan panggilan saat tidak menggunakan menunggu: A menunggu tugas dari C, yang telah dikembalikan B.

haimb
sumber
2

Ini juga membingungkan saya dan saya merasa bahwa jawaban sebelumnya mengabaikan pertanyaan Anda yang sebenarnya:

Mengapa menggunakan pengembalian menunggu konstruksi ketika Anda bisa langsung mengembalikan Tugas dari doa DoAnotherThingAsync () dalam?

Yah kadang-kadang Anda benar - benar menginginkan Task<SomeType>, tetapi sebagian besar waktu Anda benar-benar menginginkan contoh SomeType, yaitu, hasil dari tugas.

Dari kode Anda:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Seseorang yang tidak terbiasa dengan sintaks (saya, misalnya) mungkin berpikir bahwa metode ini harus mengembalikan a Task<SomeResult>, tetapi karena ditandai dengan async, itu berarti bahwa jenis pengembalian yang sebenarnya adalah SomeResult. Jika Anda hanya menggunakan return foo.DoAnotherThingAsync(), Anda akan mengembalikan Tugas, yang tidak dapat dikompilasi. Cara yang benar adalah mengembalikan hasil tugas, jadi return await.

heltonbiker
sumber
1
"tipe pengembalian aktual". Eh? async / await tidak mengubah tipe pengembalian. Dalam contoh Anda var task = DoSomethingAsync();akan memberi Anda tugas, bukanT
Sepatu
2
@ Ya ampun, saya tidak yakin saya mengerti async/awaithal itu. Untuk pemahaman saya Task task = DoSomethingAsync(),, saat Something something = await DoSomethingAsync()keduanya bekerja. Yang pertama memberi Anda tugas yang tepat, sedangkan yang kedua, karena awaitkata kunci, memberi Anda hasil dari tugas setelah selesai. Saya bisa, misalnya, miliki Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker