Singkatnya: Async / Menunggu Praktik Terbaik di .NET

Untuk mengantisipasi dimulainya kursus, "Pengembang C #" menyiapkan terjemahan materi yang menarik.




Async / Menunggu - Pendahuluan


Konstruk bahasa Async / Await telah ada sejak C # versi 5.0 (2012) dan dengan cepat menjadi salah satu pilar pemrograman .NET modern - pengembang C # yang menghargai diri sendiri harus menggunakannya untuk meningkatkan kinerja aplikasi, responsif secara keseluruhan, dan keterbacaan kode.

Async / Await membuat memperkenalkan kode asinkron secara sederhana dan menghilangkan kebutuhan bagi programmer untuk memahami detail dari pemrosesan, tetapi berapa banyak dari kita yang benar-benar tahu cara kerjanya dan apa kelebihan dan kekurangan dari metode ini? Ada banyak informasi yang berguna, tetapi itu terfragmentasi, jadi saya memutuskan untuk menulis artikel ini.

Kalau begitu, mari kita mempelajari topik.

Mesin Negara (IAsyncStateMachine)


Hal pertama yang perlu diketahui adalah bahwa di bawah tenda, setiap kali Anda memiliki metode atau fungsi dengan Async / Menunggu, kompiler sebenarnya mengubah metode Anda menjadi kelas yang dihasilkan yang mengimplementasikan antarmuka IAsyncStateMachine. Kelas ini bertanggung jawab untuk mempertahankan keadaan metode Anda selama siklus hidup operasi asinkron - kelas ini merangkum semua variabel metode Anda dalam bentuk bidang dan memecah kode Anda menjadi bagian-bagian yang dieksekusi selama mesin negara transisi antar negara, sehingga utas dapat meninggalkan metode dan saat itu akan kembali, negara tidak akan berubah.

Sebagai contoh, berikut adalah definisi kelas yang sangat sederhana dengan dua metode asinkron:

using System.Threading.Tasks;

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

Kelas dengan dua metode asinkron

Jika kita melihat kode yang dihasilkan selama perakitan, kita akan melihat sesuatu seperti ini:



Perhatikan bahwa kita memiliki 2 kelas dalam yang dihasilkan untuk kita, satu untuk setiap metode asinkron. Kelas-kelas ini berisi mesin negara untuk masing-masing metode asinkron kami.

Lebih lanjut, setelah mempelajari kode yang di-decompile <AsyncAwaitExample> d__0, kita akan melihat bahwa variabel internal kita «myVariable»sekarang menjadi bidang kelas:



Kita juga dapat melihat bidang kelas lain yang digunakan secara internal untuk mempertahankan status IAsyncStateMachine. Mesin keadaan melewati status menggunakan metodeMoveNext(), pada kenyataannya, sebuah saklar besar. Perhatikan bagaimana metode ini berlanjut di bagian yang berbeda setelah masing-masing panggilan tidak sinkron (dengan label lanjutan sebelumnya).



Itu berarti async / menunggu keanggunan datang dengan harga. Menggunakan async / menunggu sebenarnya menambah kompleksitas (yang mungkin tidak Anda sadari). Dalam logika sisi server, ini mungkin tidak kritis, tetapi khususnya ketika memprogram aplikasi seluler yang mempertimbangkan setiap siklus memori CPU dan KB, Anda harus mengingatnya, karena jumlah overhead dapat dengan cepat meningkat. Nanti dalam artikel ini, kita akan membahas praktik terbaik untuk menggunakan Async / Tunggu hanya jika perlu.

Untuk penjelasan yang cukup instruktif tentang mesin negara, tonton video ini di YouTube.

Kapan Menggunakan Async / Menunggu


Biasanya ada dua skenario di mana Async / Menunggu adalah solusi yang tepat.

  • Pekerjaan terkait I / O : Kode Anda akan mengharapkan sesuatu, seperti data dari database, membaca file, memanggil layanan web. Dalam hal ini, Anda harus menggunakan Async / Menunggu, bukan Perpustakaan Paralel Tugas.
  • Pekerjaan terkait CPU : kode Anda akan melakukan perhitungan yang rumit. Dalam hal ini, Anda harus menggunakan Async / Menunggu, tetapi Anda harus mulai bekerja di utas lain menggunakan Task.Run. Anda juga dapat mempertimbangkan menggunakan Perpustakaan Tugas Paralel .



Async sepanjang jalan


Ketika Anda mulai bekerja dengan metode asinkron, Anda akan segera menyadari bahwa sifat asinkron dari kode mulai menyebar naik turun hierarki panggilan Anda - ini berarti Anda juga harus membuat kode panggilan Anda tidak sinkron, dan sebagainya.
Anda mungkin tergoda untuk "menghentikan" ini dengan memblokir kode menggunakan Task.Result atau Task.Wait, mengonversi sebagian kecil aplikasi dan membungkusnya dalam API sinkron sehingga sisa aplikasi terisolasi dari perubahan. Sayangnya, ini adalah resep untuk membuat sulit untuk melacak kebuntuan.

Solusi terbaik untuk masalah ini adalah memungkinkan kode asinkron untuk tumbuh dalam basis kode secara alami. Jika Anda mengikuti keputusan ini, Anda akan melihat ekstensi kode asinkron ke titik masuknya, biasanya event handler atau action controller. Menyerah ke asinkron tanpa jejak!

Informasi lebih lanjut dalam artikel MSDN ini .

Jika metode ini dinyatakan sebagai async, pastikan ada yang menunggu!


Seperti yang kita bahas, ketika kompiler menemukan metode async, itu mengubah metode ini menjadi mesin negara. Jika kode Anda tidak memiliki menunggu di tubuhnya, kompiler akan menghasilkan peringatan, tetapi mesin negara akan tetap dibuat, menambahkan overhead yang tidak perlu untuk operasi yang tidak akan pernah benar-benar selesai.

Hindari async void


Void Async adalah sesuatu yang harus benar-benar dihindari. Buat aturan untuk menggunakan Tugas async dan bukannya async batal.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

Metode tugas async void dan async

Ada beberapa alasan untuk ini, termasuk:

  • Pengecualian yang dilemparkan dalam metode void async tidak dapat ditangkap di luar metode ini :
ketika pengecualian dilemparkan dari metode Tugas async atau tugas <T async >, pengecualian ini ditangkap dan ditempatkan di objek Tugas. Saat menggunakan metode async void, objek Task tidak ada, oleh karena itu, pengecualian apa pun yang dilemparkan dari metode async void akan dipanggil langsung di SynchronizationContext, yang aktif ketika metode void async diluncurkan.

Perhatikan contoh di bawah ini. Blok tangkap tidak akan pernah tercapai.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

Pengecualian yang dilemparkan dalam metode async void tidak dapat ditangkap di luar metode ini.

Bandingkan dengan kode ini, di mana alih-alih async void kita memiliki Tugas async. Dalam hal ini, tangkapan akan dapat dijangkau.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

Pengecualian ditangkap dan ditempatkan di objek Tugas.

  • Metode async void dapat menyebabkan efek samping yang tidak diinginkan jika penelepon tidak mengharapkannya asinkron : jika metode asinkron Anda tidak menghasilkan apa-apa, gunakan Tugas async (tanpa " <T >" untuk Tugas) sebagai jenis pengembalian.
  • Metode batal async sangat sulit untuk diuji : karena perbedaan dalam penanganan kesalahan dan tata letak, sulit untuk menulis unit test yang memanggil metode batal async. Tes Asynchronous MSTest hanya bekerja untuk metode asynchronous yang mengembalikan Task atau Task <T >.

Pengecualian untuk praktik ini adalah pengendali event asinkron. Tetapi bahkan dalam kasus ini, disarankan untuk meminimalkan kode yang ditulis dalam handler itu sendiri - mengharapkan metode Tugas async yang berisi logika.

Informasi lebih lanjut dalam artikel MSDN ini .

Lebih suka mengembalikan tugas daripada menunggu kembali


Seperti yang sudah dibahas, setiap kali Anda mendeklarasikan metode asynchronous, kompiler membuat kelas mesin negara yang benar-benar membungkus logika metode Anda. Ini menambahkan overhead tertentu yang dapat terakumulasi, terutama untuk perangkat seluler, di mana kami memiliki batasan sumber daya yang lebih ketat.

Kadang-kadang suatu metode tidak harus asinkron, tetapi mengembalikan Tugas <T >dan memungkinkan pihak lain untuk menanganinya. Jika kalimat terakhir dari kode Anda adalah menunggu pengembalian, Anda harus mempertimbangkan refactoring sehingga jenis pengembalian metode ini adalah Tugas <T>(bukan T async). Karena itu, Anda menghindari membuat mesin negara, yang membuat kode Anda lebih fleksibel. Satu-satunya kasus yang benar-benar ingin kita tunggu adalah ketika kita melakukan sesuatu dengan async Task yang menghasilkan kelanjutan dari metode ini.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

Lebih suka mengembalikan Tugas daripada kembali menunggu.

Perhatikan bahwa jika kita tidak memiliki menunggu dan bukannya mengembalikan Tugas <T >, pengembalian terjadi segera, jadi jika kode berada di dalam blok coba / tangkap, pengecualian tidak akan ditangkap. Demikian pula, jika kode berada di dalam blok menggunakan, ia akan segera menghapus objek. Lihat tip selanjutnya.

Jangan bungkus kembali tugas di dalam try..catch {} atau menggunakan {} blok


Tugas Kembali dapat menyebabkan perilaku tidak terdefinisi ketika digunakan di dalam blok try..catch (pengecualian yang dilemparkan oleh metode asinkron tidak akan pernah ditangkap) atau di dalam blok menggunakan, karena tugas akan segera dikembalikan.

Jika Anda perlu membungkus kode asinkron Anda dalam try..catch atau menggunakan blok, gunakan return await.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

Jangan membungkus tugas kembali di dalam blok try..catch{}atauusing{} .

Informasi lebih lanjut di utas ini tentang stack overflow.

Hindari menggunakan .Wait()atau .Result- gunakan sajaGetAwaiter().GetResult()


Jika Anda perlu memblokir menunggu Tugas Async untuk menyelesaikan, gunakan GetAwaiter().GetResult(). Waitdan Resultmasukkan pengecualian AggregateException, yang mempersulit penanganan kesalahan. Keuntungannya GetAwaiter().GetResult()adalah ia mengembalikan pengecualian biasa AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

Jika Anda perlu memblokir menunggu Tugas Async untuk diselesaikan, gunakan GetAwaiter().GetResult().

informasi selengkapnya di tautan ini .

Jika metode ini tidak sinkron, tambahkan akhiran Async ke namanya


Ini adalah konvensi yang digunakan dalam .NET untuk lebih mudah membedakan antara metode sinkron dan asinkron (dengan pengecualian event handler atau metode pengontrol web, tetapi mereka masih tidak boleh secara eksplisit dipanggil oleh kode Anda).

Metode pustaka asinkron harus menggunakan Task.ConfigureAwait (false) untuk meningkatkan kinerja



NET Framework. Memiliki konsep "konteks sinkronisasi," yang merupakan cara untuk "kembali ke tempat Anda sebelumnya." Setiap kali Tugas sedang menunggu, itu menangkap konteks sinkronisasi saat ini sebelum menunggu.

Setelah Tugas selesai .Post(), metode konteks sinkronisasi dipanggil , yang melanjutkan pekerjaan dari sebelumnya. Ini berguna untuk kembali ke utas antarmuka pengguna atau untuk kembali ke konteks ASP.NET yang sama, dll.
Saat menulis kode pustaka, Anda jarang perlu kembali ke konteks sebelumnya. Ketika Task.ConfigureAwait (false) digunakan, kode tidak lagi mencoba untuk melanjutkan dari tempat sebelumnya, sebaliknya, jika mungkin, kode keluar di utas yang menyelesaikan tugas, yang menghindari pengalihan konteks. Ini sedikit meningkatkan kinerja dan dapat membantu menghindari kebuntuan.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

Biasanya, gunakan ConfigureAwait (false) untuk proses server dan kode pustaka.
Ini sangat penting ketika metode perpustakaan disebut sejumlah besar kali, untuk respon yang lebih baik.

Biasanya, gunakan ConfigureAwait (false) untuk proses server secara umum. Kami tidak peduli utas mana yang digunakan untuk melanjutkan, tidak seperti aplikasi yang kami perlukan untuk kembali ke utas antarmuka pengguna.

Sekarang ... Di ASP.NET Core, Microsoft telah menghapus SynchronizationContext, jadi secara teoritis Anda tidak memerlukan itu. Tetapi jika Anda menulis kode perpustakaan yang berpotensi digunakan kembali di aplikasi lain (mis. Aplikasi UI, Legacy ASP.NET, Formulir Xamarin), ini tetap merupakan praktik terbaik .

Untuk penjelasan yang baik tentang konsep ini, tonton video ini .

Laporan kemajuan tugas tidak sinkron


Kasus penggunaan yang cukup umum untuk metode asinkron adalah bekerja di latar belakang, membebaskan utas antarmuka pengguna untuk tugas-tugas lain, dan memelihara daya tanggap. Dalam skenario ini, Anda mungkin ingin melaporkan kemajuan kembali ke antarmuka pengguna sehingga pengguna dapat memantau kemajuan proses dan berinteraksi dengan operasi.

Untuk mengatasi masalah umum ini, .NET menyediakan antarmuka IProgress <T >, yang menyediakan metode Laporan <T >, yang dipanggil oleh tugas asinkron untuk melaporkan progres ke pemanggil. Antarmuka ini diterima sebagai parameter dari metode asinkron - penelepon harus menyediakan objek yang mengimplementasikan antarmuka ini.

.NET menyediakan Progress <T >, implementasi standar IProgress <T >, yang sebenarnya direkomendasikan, karena menangani semua logika tingkat rendah yang terkait dengan menyimpan dan memulihkan konteks sinkronisasi. Progress <T >juga menyediakan acara Tindakan <T dan panggilan balik >- keduanya dipanggil saat tugas melaporkan kemajuan.

Bersama-sama, IProgress <T >dan Progress <T >menyediakan cara mudah untuk mentransfer informasi kemajuan dari tugas latar belakang ke utas antarmuka pengguna.

Harap dicatat bahwa <T>itu bisa berupa nilai sederhana, seperti int, atau objek yang menyediakan informasi kemajuan kontekstual, seperti persentase penyelesaian, deskripsi string operasi saat ini, ETA, dan sebagainya.
Pertimbangkan seberapa sering Anda melaporkan kemajuan. Bergantung pada operasi yang Anda lakukan, Anda mungkin menemukan bahwa kode Anda melaporkan kemajuan beberapa kali per detik, yang dapat mengakibatkan antarmuka pengguna menjadi kurang responsif. Dalam skenario seperti itu, disarankan agar kemajuan dilaporkan pada interval yang lebih besar.

Informasi lebih lanjut dalam artikel ini di blog Microsoft .NET resmi.

Batalkan Tugas Asinkron


Kasus penggunaan umum lainnya untuk tugas latar belakang adalah kemampuan untuk membatalkan eksekusi. .NET menyediakan kelas CancelledToken. Metode asinkron menerima objek Pembatalan, yang kemudian dibagikan oleh kode pesta panggilan dan metode asinkron, sehingga menyediakan mekanisme untuk pensinyalan pembatalan.

Dalam kasus yang paling umum, pembatalan terjadi sebagai berikut:

  1. Penelepon membuat objek PembatalanTokenSource.
  2. Penelepon memanggil API asinkron yang dibatalkan dan melewati CancurToken dari CancurTokenSource (CancurTokenSource.Token).
  3. Penelepon meminta pembatalan menggunakan objek PembatalanTokenSource (PembatalanTokenSource.Cancel ()).
  4. Tugas mengkonfirmasi pembatalan dan membatalkan itu sendiri, biasanya menggunakan metode PembatalanToken.ThrowIfCancellationRequested.

Harap perhatikan bahwa agar mekanisme ini berfungsi, Anda harus menulis kode untuk memeriksa pembatalan yang diminta secara berkala (mis. Pada setiap iterasi kode Anda atau pada breakpoint alami dalam logika). Idealnya, setelah permintaan dibatalkan, tugas asinkron harus dibatalkan secepat mungkin.

Anda harus mempertimbangkan untuk menggunakan undo untuk semua metode yang bisa memakan waktu lama untuk diselesaikan.

Informasi lebih lanjut dalam artikel ini di blog Microsoft .NET resmi.

Laporan Kemajuan dan Pembatalan - Contoh


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

Menunggu beberapa saat


Jika Anda perlu menunggu beberapa saat (misalnya, coba lagi untuk memeriksa ketersediaan sumber daya), pastikan untuk menggunakan Task.Delay - jangan pernah menggunakan Thread. Tidur dalam skenario ini.

Menunggu beberapa tugas tidak sinkron diselesaikan


Gunakan Task.WaitAny untuk menunggu penyelesaian tugas apa pun. Gunakan Task.WaitAll untuk menunggu semua tugas selesai.

Apakah saya harus buru-buru beralih ke C # 7 atau 8? Mendaftar untuk webinar gratis untuk membahas topik ini.

All Articles