async / menunggu - kapan mengembalikan Task vs void?


503

Di bawah skenario apa yang ingin digunakan seseorang

public async Task AsyncMethod(int num)

dari pada

public async void AsyncMethod(int num)

Satu-satunya skenario yang bisa saya pikirkan adalah jika Anda membutuhkan tugas untuk dapat melacak kemajuannya.

Selain itu, dalam metode berikut, apakah async dan menunggu kata kunci tidak perlu?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
Perhatikan bahwa metode async harus selalu diakhiri dengan nama Async . Contohnya Foo()akan menjadi FooAsync().
Fred

30
@ Fred Kebanyakan, tetapi tidak selalu. Ini hanya konvensi dan pengecualian yang diterima untuk konvensi ini adalah dengan kelas berbasis acara atau kontrak antarmuka, lihat MSDN . Misalnya, Anda tidak boleh mengganti nama pengendali acara umum, seperti Button1_Klik.
Ben

14
Hanya catatan Anda tidak harus menggunakan Thread.Sleepdengan tugas-tugas Anda, Anda harus await Task.Delay(num)sebaliknya
Bob Vale

45
@ Fred Saya tidak setuju dengan ini, IMO menambahkan akhiran async hanya boleh digunakan ketika Anda menyediakan antarmuka dengan opsi sinkronisasi dan async. Smurf memberi nama sesuatu dengan async ketika hanya ada satu maksud yang tidak ada gunanya. Contoh kasusnya Task.Delayadalah tidak Task.AsyncDelaysemua metode dalam tugas adalah Async
Tidak dicintai

11
Saya punya masalah yang menarik pagi ini dengan metode pengontrol webapi 2, yang dinyatakan sebagai async voidgantinya async Task. Metode macet karena menggunakan objek konteks Kerangka Entitas yang dideklarasikan sebagai anggota controller dibuang sebelum metode selesai dieksekusi. Kerangka kerja membuang controller sebelum metode selesai dieksekusi. Saya mengubah metode menjadi Tugas async dan berhasil.
Costa

Jawaban:


417

1) Biasanya, Anda ingin mengembalikan a Task. Pengecualian utama seharusnya ketika Anda harus memiliki voidtipe pengembalian (untuk acara). Jika tidak ada alasan untuk melarang penelepon memiliki awaittugas Anda, mengapa tidak mengizinkannya?

2) asyncmetode yang mengembalikan voidkhusus dalam aspek lain: mereka mewakili operasi async tingkat atas , dan memiliki aturan tambahan yang ikut bermain ketika tugas Anda mengembalikan pengecualian. Cara termudah untuk menunjukkan perbedaannya adalah dengan contoh:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fPengecualian selalu "diamati". Pengecualian yang meninggalkan metode asinkron tingkat atas hanya diperlakukan seperti pengecualian lain yang tidak ditangani. gPengecualian tidak pernah diamati. Ketika pengumpul sampah datang untuk membersihkan tugas, ia melihat bahwa tugas menghasilkan pengecualian, dan tidak ada yang menangani pengecualian. Ketika itu terjadi, TaskScheduler.UnobservedTaskExceptionpawang berlari. Anda seharusnya tidak pernah membiarkan ini terjadi. Untuk menggunakan contoh Anda,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Ya, gunakan asyncdan di awaitsini, mereka memastikan metode Anda masih berfungsi dengan benar jika ada pengecualian.

untuk informasi lebih lanjut lihat: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
Maksud saya falih-alih gdalam komentar saya. Pengecualian dari fditeruskan ke SynchronizationContext. gakan naik UnobservedTaskException, tetapi proses UTEtidak lagi macet jika tidak ditangani. Ada beberapa situasi di mana itu dapat diterima untuk memiliki "pengecualian asinkron" seperti ini yang diabaikan.
Stephen Cleary

3
Jika Anda memiliki WhenAnybeberapa Tasks yang menghasilkan pengecualian. Anda sering hanya perlu menangani yang pertama, dan Anda sering ingin mengabaikan yang lain.
Stephen Cleary

1
@StephenCleary Terima kasih, saya rasa itu adalah contoh yang baik, meskipun itu tergantung pada alasan Anda menelepon WhenAnydi awal apakah tidak apa-apa untuk mengabaikan pengecualian lain: kasus penggunaan utama yang saya miliki untuk itu masih berakhir menunggu tugas yang tersisa ketika ada selesai , dengan atau tanpa pengecualian.

10
Saya agak bingung mengapa Anda merekomendasikan mengembalikan Tugas, bukannya batal. Seperti yang Anda katakan, f () akan melempar dan pengecualian tetapi g () tidak akan. Bukankah lebih baik diberitahukan pengecualian latar belakang ini?
user981225

2
@ user981225 Memang, tapi itu kemudian menjadi tanggung jawab penelepon g: setiap metode yang memanggil g harus async dan gunakan menunggu juga. Ini adalah pedoman, bukan aturan keras, Anda dapat memutuskan bahwa dalam program spesifik Anda, akan lebih mudah mengembalikan void.

40

Saya telah menemukan artikel yang sangat berguna tentang ini asyncdan voidditulis oleh Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

Intinya adalah bahwa async+voidcan crash sistem dan biasanya harus digunakan hanya pada event handler sisi UI.

Alasan di balik ini adalah Konteks Sinkronisasi yang digunakan oleh AsyncVoidMethodBuilder, karena tidak ada dalam contoh ini. Ketika tidak ada Konteks Sinkronisasi ambient, setiap pengecualian yang ditangani oleh tubuh metode async void diulang kembali di ThreadPool. Meskipun tampaknya tidak ada tempat logis lain di mana pengecualian tidak tertangani semacam itu dapat dilemparkan, efek yang disayangkan adalah bahwa proses sedang dihentikan, karena pengecualian yang tidak tertangani pada ThreadPool secara efektif menghentikan proses sejak .NET 2.0. Anda dapat mencegat semua pengecualian tidak tertangani menggunakan acara AppDomain.UnhandledException, tetapi tidak ada cara untuk memulihkan proses dari acara ini.

Saat menulis event handler UI, metode async void entah bagaimana tidak menyakitkan karena pengecualian diperlakukan dengan cara yang sama ditemukan dalam metode non-async; mereka dilemparkan ke Dispatcher. Ada kemungkinan untuk pulih dari pengecualian seperti itu, dengan lebih dari benar untuk sebagian besar kasus. Di luar event handler UI, metode batal async entah bagaimana berbahaya untuk digunakan dan mungkin tidak mudah ditemukan.


Apakah itu benar? Saya pikir pengecualian dalam metode async ditangkap dalam Tugas. Dan juga afaik selalu ada Konteks sinkronisasi deafault
NM

30

Saya mendapat ide yang jelas dari pernyataan ini.

  1. Metode async void memiliki semantik penanganan kesalahan yang berbeda. Ketika pengecualian dilempar keluar dari tugas async atau metode tugas async, pengecualian itu ditangkap dan ditempatkan pada objek tugas. Dengan metode batal async, tidak ada objek Tugas, jadi setiap pengecualian yang dibuang dari metode batal async akan dimunculkan langsung pada SynchronizationContext (SynchronizationContext mewakili lokasi "di mana" kode mungkin dieksekusi.) Yang aktif ketika metode batal async mulai

Pengecualian dari Metode Void Async Tidak Dapat Ditangkap dengan Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Pengecualian ini dapat diamati menggunakan AppDomain.UnhandledException atau acara catch-all yang serupa untuk aplikasi GUI / ASP.NET, tetapi menggunakan peristiwa itu untuk penanganan pengecualian reguler adalah resep untuk ketidakteraturan (ini membuat crash aplikasi).

  1. Metode void Async memiliki semantik penulisan yang berbeda. Metode Async mengembalikan Tugas atau Tugas dapat dengan mudah dikomposisikan menggunakan menunggu, Task.WhenAny, Task.WhenAll dan sebagainya. Metode async yang mengembalikan batal tidak memberikan cara mudah untuk memberi tahu kode panggilan yang telah mereka selesaikan. Sangat mudah untuk memulai beberapa metode batal asinkron, tetapi tidak mudah untuk menentukan kapan mereka selesai. Metode batal async akan memberi tahu SynchronizationContext mereka ketika mereka mulai dan selesai, tetapi SynchronizationContext kustom adalah solusi kompleks untuk kode aplikasi reguler.

  2. Metode Async Void berguna saat menggunakan pengendali acara sinkron karena mereka menaikkan pengecualian mereka langsung pada SynchronizationContext, yang mirip dengan bagaimana perilaku penangan sinkron berperilaku

Untuk lebih jelasnya periksa tautan ini https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

Masalah dengan memanggil async void adalah bahwa Anda bahkan tidak mendapatkan tugas kembali, Anda tidak memiliki cara untuk mengetahui kapan tugas fungsi telah selesai (lihat https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Berikut adalah tiga cara untuk memanggil fungsi async:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

Dalam semua kasus, fungsi diubah menjadi rantai tugas. Perbedaannya adalah apa fungsi yang dikembalikan.

Dalam kasus pertama, fungsi mengembalikan tugas yang akhirnya menghasilkan t.

Dalam kasus kedua, fungsi mengembalikan tugas yang tidak memiliki produk, tetapi Anda masih bisa menunggu untuk mengetahui kapan ia berjalan sampai selesai.

Kasus ketiga adalah yang buruk. Kasus ketiga seperti kasus kedua, kecuali bahwa Anda bahkan tidak mendapatkan tugas kembali. Anda tidak memiliki cara untuk mengetahui kapan tugas fungsi telah selesai.

Casing async void adalah "api dan lupakan": Anda memulai rantai tugas, tetapi Anda tidak peduli kapan selesai. Ketika fungsi kembali, yang Anda tahu adalah bahwa semuanya hingga menunggu pertama telah dieksekusi. Semuanya setelah penantian pertama akan berjalan pada titik yang tidak ditentukan di masa depan yang Anda tidak memiliki akses.


6

Saya pikir Anda dapat menggunakan async voiduntuk memulai operasi latar belakang juga, asalkan Anda berhati-hati untuk menangkap pengecualian. Pikiran?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
Masalah dengan memanggil async void adalah bahwa Anda bahkan tidak mendapatkan tugas kembali, Anda tidak memiliki cara untuk mengetahui kapan tugas fungsi telah selesai
user8128167

Ini masuk akal untuk operasi latar belakang (jarang) di mana Anda tidak peduli dengan hasilnya dan Anda tidak ingin pengecualian mempengaruhi operasi lain. Kasing yang biasa digunakan adalah mengirim acara log ke server log. Anda ingin ini terjadi di latar belakang, dan Anda tidak ingin penangan layanan / permintaan Anda gagal jika server log turun. Tentu saja, Anda harus menangkap semua pengecualian, atau proses Anda akan dihentikan. Atau ada cara yang lebih baik untuk mencapai ini?
Florian Winter

Pengecualian dari Metode Void Async Tidak Dapat Ditangkap dengan Catch. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi

3
@ SiZi tangkapannya adalah DALAM metode void async seperti yang ditunjukkan dalam contoh dan ditangkap.
bboyle1234

Bagaimana ketika persyaratan Anda berubah sehingga tidak perlu bekerja jika Anda tidak dapat mencatatnya - maka Anda harus mengubah tanda tangan di seluruh API Anda, alih-alih hanya mengubah logika yang saat ini memiliki // Abaikan Pengecualian
Milney

0

Jawaban saya sederhana, Anda tidak dapat menunggu metode kosong

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Jadi, jika metode ini async lebih baik menunggu, karena Anda bisa kehilangan keuntungan async.


-2

Menurut dokumentasi Microsoft , sebaiknya TIDAK PERNAH digunakanasync void

Jangan lakukan ini: Contoh berikut menggunakan async voidyang membuat permintaan HTTP selesai ketika menunggu pertama tercapai:

  • Yang SELALU praktik yang buruk dalam aplikasi ASP.NET Core.

  • Mengakses HttpResponse setelah permintaan HTTP selesai.

  • Menghancurkan proses.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.