Bagaimana cara memanggil metode async dengan aman di C # tanpa menunggu


322

Saya memiliki asyncmetode yang tidak mengembalikan data:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

Saya memanggil ini dari metode lain yang mengembalikan beberapa data:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Memanggil MyAsyncMethod()tanpa menunggu menyebabkan " Karena panggilan ini tidak ditunggu, metode saat ini terus berjalan sebelum panggilan selesai " peringatan di studio visual. Pada halaman peringatan itu disebutkan:

Anda harus mempertimbangkan untuk menekan peringatan hanya jika Anda yakin tidak ingin menunggu panggilan tidak sinkron selesai dan bahwa metode yang dipanggil tidak akan memunculkan pengecualian .

Saya yakin saya tidak ingin menunggu sampai panggilan selesai; Saya tidak perlu atau punya waktu untuk itu. Tapi panggilan itu bisa menimbulkan pengecualian.

Saya telah menemukan masalah ini beberapa kali dan saya yakin itu adalah masalah umum yang harus memiliki solusi bersama.

Bagaimana saya memanggil metode async dengan aman tanpa menunggu hasilnya?

Memperbarui:

Bagi orang yang menyarankan agar saya menunggu hasilnya, ini adalah kode yang merespons permintaan web pada layanan web kami (ASP.NET Web API). Menunggu dalam konteks UI membuat utas UI tetap bebas, tetapi menunggu dalam panggilan permintaan web akan menunggu Tugas selesai sebelum menanggapi permintaan, sehingga meningkatkan waktu respons tanpa alasan.


Mengapa tidak membuat metode penyelesaian saja dan abaikan saja di sana? Karena jika itu berjalan di latar belakang utas. Maka itu tidak akan menghentikan program Anda untuk mengakhiri.
Yahya

1
Jika Anda tidak ingin menunggu hasilnya, satu-satunya pilihan adalah mengabaikan / menekan peringatan. Jika Anda tidak ingin menunggu hasil / pengecualian kemudianMyAsyncMethod().Wait()
Peter Ritchie

1
Tentang hasil edit Anda: itu tidak masuk akal bagi saya. Katakanlah respons dikirim ke klien 1 detik setelah permintaan, dan 2 detik kemudian metode async Anda melempar pengecualian. Apa yang akan Anda lakukan dengan pengecualian itu? Anda tidak dapat mengirimnya ke klien, jika respons Anda sudah dikirim. Apa lagi yang akan Anda lakukan dengan itu?

2
@Romoku Cukup adil. Mengasumsikan seseorang melihat log. :)

2
Variasi pada skenario ASP.NET Web API adalah API Web yang di -hosting sendiri dalam proses yang tahan lama (seperti, katakanlah, layanan Windows), di mana permintaan membuat tugas latar belakang yang panjang untuk melakukan sesuatu yang mahal, tetapi masih ingin dapatkan respons dengan cepat dengan HTTP 202 (Diterima).
David Rubin

Jawaban:


187

Jika Anda ingin mendapatkan pengecualian "asinkron", Anda dapat melakukan:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Ini akan memungkinkan Anda untuk menangani pengecualian pada utas selain utas "utama". Ini berarti Anda tidak perlu "menunggu" untuk panggilan MyAsyncMethod()dari utas yang memanggil MyAsyncMethod; tetapi, masih memungkinkan Anda untuk melakukan sesuatu dengan pengecualian - tetapi hanya jika pengecualian terjadi.

Memperbarui:

secara teknis, Anda dapat melakukan sesuatu yang mirip dengan await:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... yang akan berguna jika Anda perlu secara khusus menggunakan try/ catch(atau using) tetapi saya menemukan ContinueWithuntuk menjadi sedikit lebih eksplisit karena Anda harus tahu apa ConfigureAwait(false)artinya.


7
Saya mengubah ini menjadi metode ekstensi pada Task: public static class AsyncUtility {public static void PerformAsyncTaskWithoutAwait (tugas Tugas ini, Aksi <Task> exceptionHandler) {var dummy = task.ContinueWith (t => exceptionHandler (t), TaskContinuationOptions.OnlyOnFaulted); }} Penggunaan: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat ("Terjadi kesalahan saat memanggil MyAsyncMethod: \ n {0}", t.Exception));
Mark Avenius

2
downvoter. Komentar? Jika ada yang salah dalam jawabannya, saya ingin tahu dan / atau memperbaikinya.
Peter Ritchie

2
Hai - Saya tidak downvote, tapi ... Bisakah Anda menjelaskan pembaruan Anda? Panggilan itu seharusnya tidak ditunggu, jadi jika Anda melakukannya seperti itu, maka Anda menunggu panggilan itu, Anda hanya tidak melanjutkan pada konteks yang ditangkap ...
Bartosz

1
"tunggu" bukan deskripsi yang benar dalam konteks ini. Baris setelah perintah ConfiguratAwait(false)tidak dieksekusi sampai tugas selesai, tetapi utas saat ini tidak "menunggu" (yaitu blok) untuk itu, baris berikutnya dipanggil secara tidak serempak ke doa menunggu. Tanpa ConfigureAwait(false)baris berikutnya akan dieksekusi pada konteks permintaan web asli. Dengan ConfigurateAwait(false)itu dieksekusi dalam konteks yang sama dengan metode async (tugas), membebaskan konteks / utas asli untuk melanjutkan ...
Peter Ritchie

15
Versi ContinueWith tidak sama dengan versi coba {await} catch {}. Di versi pertama, semuanya setelah ContinueWith () akan segera dijalankan. Tugas awal dipecat dan dilupakan. Di versi kedua, semuanya setelah tangkapan {} akan dieksekusi hanya setelah tugas awal selesai. Versi kedua setara dengan "menunggu MyAsyncMethod (). LanjutkanDengan (t => Konsol.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis

67

Pertama-tama Anda harus mempertimbangkan membuat GetStringDatasuatu asyncmetode dan awaitmengembalikannya dari tugas MyAsyncMethod.

Jika Anda benar-benar yakin bahwa Anda tidak perlu menangani pengecualian dari MyAsyncMethod atau tahu kapan itu selesai, maka Anda dapat melakukan ini:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

BTW, ini bukan "masalah umum". Sangat jarang ingin mengeksekusi beberapa kode dan tidak peduli apakah itu selesai dan tidak peduli apakah itu berhasil diselesaikan.

Memperbarui:

Karena Anda menggunakan ASP.NET dan ingin kembali lebih awal, Anda mungkin menemukan posting blog saya pada subjek yang bermanfaat . Namun, ASP.NET tidak dirancang untuk ini, dan tidak ada jaminan bahwa kode Anda akan berjalan setelah respons dikembalikan. ASP.NET akan melakukan yang terbaik untuk menjalankannya, tetapi tidak dapat menjaminnya.

Jadi, ini adalah solusi yang baik untuk sesuatu yang sederhana seperti melempar sebuah acara ke log di mana itu tidak benar-benar peduli jika Anda kehilangan beberapa di sana-sini. Ini bukan solusi yang baik untuk segala jenis operasi bisnis-kritis. Dalam situasi itu, Anda harus mengadopsi arsitektur yang lebih kompleks, dengan cara yang gigih untuk menyelamatkan operasi (misalnya, Antrian Azure, MSMQ) dan proses latar belakang yang terpisah (misalnya, Peran Pekerja Azure, Layanan Win32) untuk memprosesnya.


2
Saya pikir Anda mungkin salah paham. Saya melakukan perawatan jika melempar pengecualian dan gagal, tapi saya tidak mau harus menunggu metode sebelum kembali data saya. Lihat juga edit saya tentang konteks tempat saya bekerja jika itu membuat perbedaan.
George Powell

5
@ GeorgePowell: Sangat berbahaya memiliki kode yang berjalan dalam konteks ASP.NET tanpa permintaan aktif. Saya memiliki posting blog yang dapat membantu Anda , tetapi tanpa mengetahui lebih banyak masalah Anda, saya tidak bisa mengatakan apakah saya akan merekomendasikan pendekatan itu atau tidak.
Stephen Cleary

@StephenCleary Saya memiliki kebutuhan yang sama. Dalam contoh saya, saya memiliki / membutuhkan mesin pengolah batch untuk berjalan di cloud, saya akan "ping" titik akhir untuk memulai pemrosesan batch, tetapi saya ingin segera kembali. Karena ping itu memulainya, ia dapat menangani semuanya dari sana. Jika ada pengecualian yang dilemparkan, maka mereka hanya akan masuk dalam tabel "BatchProcessLog / Error" saya ...
ganders

8
Di C # 7, Anda bisa menggantinya var _ = MyAsyncMethod();dengan _ = MyAsyncMethod();. Ini masih menghindari CS4014 peringatan, tetapi membuatnya sedikit lebih eksplisit bahwa Anda tidak menggunakan variabel.
Brian

48

Jawaban oleh Peter Ritchie adalah apa yang saya inginkan, dan artikel Stephen Cleary tentang kembali lebih awal di ASP.NET sangat membantu.

Namun, sebagai masalah yang lebih umum (tidak spesifik untuk konteks ASP.NET) aplikasi Konsol berikut menunjukkan penggunaan dan perilaku jawaban Peter menggunakan Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()kembali lebih awal tanpa menunggu MyAsyncMethod()dan pengecualian yang dilemparkan MyAsyncMethod()ditangani dalam OnMyAsyncMethodFailed(Task task)dan bukan di try/ catchsekitarGetStringData()


9
Hapus Console.ReadLine();dan tambahkan sedikit tidur / keterlambatan MyAsyncMethoddan Anda tidak akan pernah melihat pengecualian.
tymtam

17

Saya berakhir dengan solusi ini:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}


2

Saya kira timbul pertanyaan, mengapa Anda perlu melakukan ini? Alasan asyncdalam C # 5.0 adalah agar Anda dapat menunggu hasilnya. Metode ini sebenarnya tidak asinkron, tetapi hanya dipanggil pada suatu waktu agar tidak terlalu mengganggu thread saat ini.

Mungkin lebih baik memulai utas dan membiarkannya selesai sendiri.


2
asyncsedikit lebih dari sekadar "menunggu" hasilnya. "menunggu" menyiratkan bahwa garis-garis yang mengikuti "menunggu" dieksekusi secara asinkron pada utas yang sama yang memanggil "menunggu". Ini bisa dilakukan tanpa "menunggu", tentu saja, tetapi Anda akhirnya memiliki banyak delegasi dan kehilangan tampilan dan rasa kode yang berurutan (serta kemampuan untuk menggunakan usingdan try/catch...
Peter Ritchie

1
@PeterRitchie Anda dapat memiliki metode yang secara fungsional tidak sinkron, tidak menggunakan awaitkata kunci, dan tidak menggunakan asynckata kunci, tetapi tidak ada gunanya menggunakan asynckata kunci tanpa juga menggunakan awaitdefinisi metode itu.
Servy

1
@PeterRitchie Dari pernyataan: " asyncsedikit lebih dari sekadar" menunggu "hasilnya." Kata asynckunci (Anda menyiratkan kata kunci dengan melampirkannya di backticks) berarti tidak lebih dari menunggu hasilnya. Ini tidak sinkron, seperti konsep CS umum, itu berarti lebih dari sekadar menunggu hasil.
Servy

1
@Servy asyncmenciptakan mesin status yang mengelola semua menunggu dalam metode async. Jika tidak ada awaits dalam metode itu masih menciptakan mesin negara itu - tetapi metode ini tidak sinkron. Dan jika asyncmetode ini kembali void, tidak ada yang menunggu. Jadi, ini lebih dari sekedar menunggu hasil.
Peter Ritchie

1
@PeterRitchie Selama metode mengembalikan tugas, Anda bisa menunggu. Tidak perlu mesin negara, atau asynckata kunci. Yang perlu Anda lakukan (dan semua yang benar-benar terjadi pada akhirnya dengan mesin negara dalam kasus khusus) adalah bahwa metode ini dijalankan secara sinkron dan kemudian dibungkus dengan tugas yang selesai. Saya kira secara teknis Anda tidak hanya menghapus mesin negara; Anda menghapus mesin negara dan kemudian menelepon Task.FromResult. Saya berasumsi Anda (dan juga penulis kompiler) dapat menambahkan addendum sendiri.
Servy

0

Pada teknologi dengan loop pesan (tidak yakin apakah ASP adalah salah satunya), Anda dapat memblokir loop dan memproses pesan sampai tugas selesai, dan menggunakan ContinueWith untuk membuka blokir kode:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Pendekatan ini mirip dengan pemblokiran pada ShowDialog dan tetap menjaga UI responsif.


0

Saya terlambat ke pesta di sini, tapi ada perpustakaan luar biasa yang saya gunakan yang belum saya lihat yang dirujuk dalam jawaban lain

https://github.com/brminnick/AsyncAwaitBestPractices

Jika Anda perlu "Fire And Forget", Anda memanggil metode ekstensi pada tugas.

Melewati tindakan pada Eksepsi ke panggilan memastikan bahwa Anda mendapatkan yang terbaik dari kedua dunia - tidak perlu menunggu eksekusi dan memperlambat pengguna Anda, sambil mempertahankan kemampuan untuk menangani pengecualian dengan cara yang anggun.

Dalam contoh Anda, Anda akan menggunakannya seperti ini:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

Ini juga memberikan AsyncCommands yang dapat dilaksanakan dengan mengimplementasikan ICommand di luar kotak yang bagus untuk solusi MVVM Xamarin saya


-1

Solusinya adalah mulai HttpClient ke dalam tugas eksekusi lain tanpa konteks penyatuan:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);

-1

Jika Anda benar-benar ingin melakukan ini. Hanya untuk mengatasi "Panggil metode async dalam C # tanpa menunggu", Anda dapat menjalankan metode async di dalam a Task.Run. Pendekatan ini akan menunggu sampai MyAsyncMethodselesai.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitsecara asinkron membuka bungkus Resulttugas Anda, sedangkan hanya menggunakan Hasil akan memblokir sampai tugas selesai.


-1

Metode async biasanya mengembalikan kelas Tugas. Jika Anda menggunakan Wait()metode atau Resultproperti dan pelemparan kode pengecualian - tipe pengecualian dimasukkan ke dalamnya AggregateException- maka Anda perlu kueri Exception.InnerExceptionuntuk menemukan pengecualian yang benar.

Tapi itu juga mungkin untuk digunakan .GetAwaiter().GetResult()sebagai gantinya - itu juga akan menunggu tugas async, tetapi tidak akan membungkus pengecualian.

Jadi, inilah contoh singkatnya:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Anda mungkin ingin juga dapat mengembalikan beberapa parameter dari fungsi async - yang dapat dicapai dengan memberikan tambahan Action<return type>ke fungsi async, misalnya seperti ini:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

Harap perhatikan bahwa metode async biasanya memiliki ASyncpenamaan suffix, hanya untuk dapat menghindari tabrakan antara fungsi sinkronisasi dengan nama yang sama. (Misalnya FileStream.ReadAsync) - Saya telah memperbarui nama fungsi untuk mengikuti rekomendasi ini.

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.