Gunakan async / await secara efektif dengan ASP.NET Web API

114

Saya mencoba menggunakan async/awaitfitur ASP.NET dalam proyek API Web saya. Saya tidak begitu yakin apakah itu akan membuat perbedaan dalam kinerja layanan API Web saya. Temukan di bawah alur kerja dan kode contoh dari aplikasi saya.

Alur Kerja:

Aplikasi UI → Titik akhir API Web (pengontrol) → Metode panggilan di lapisan layanan API Web → Panggil layanan web eksternal lainnya. (Di sini kita memiliki interaksi DB, dll.)

Pengontrol:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Lapisan Layanan:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Saya menguji kode di atas dan berfungsi. Tapi saya tidak yakin apakah itu penggunaan yang benar async/await. Silakan bagikan pemikiran Anda.

arp
sumber

Jawaban:

202

Saya tidak begitu yakin apakah itu akan membuat perbedaan dalam kinerja API saya.

Ingatlah bahwa manfaat utama kode asinkron di sisi server adalah skalabilitas . Ini tidak akan secara ajaib membuat permintaan Anda berjalan lebih cepat. Saya membahas beberapa asyncpertimbangan "harus saya gunakan " dalam artikelasync saya di ASP.NET .

Saya pikir kasus penggunaan Anda (memanggil API lain) sangat sesuai untuk kode asinkron, ingatlah bahwa "asinkron" tidak berarti "lebih cepat". Pendekatan terbaik adalah pertama-tama membuat UI Anda responsif dan asinkron; ini akan membuat aplikasi Anda terasa lebih cepat meskipun sedikit lebih lambat.

Sejauh kode berjalan, ini tidak asinkron:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Anda memerlukan implementasi yang benar-benar asinkron untuk mendapatkan manfaat skalabilitas dari async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Atau (jika logika Anda dalam metode ini benar-benar hanya sebuah pass-through):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Perhatikan bahwa lebih mudah untuk bekerja dari "dalam ke luar" daripada "dari luar ke dalam" seperti ini. Dengan kata lain, jangan mulai dengan tindakan pengontrol asinkron lalu paksa metode hilir menjadi asinkron. Sebagai gantinya, identifikasi operasi asinkron secara alami (memanggil API eksternal, kueri database, dll), dan buat yang asinkron di tingkat paling bawah terlebih dahulu ( Service.ProcessAsync). Kemudian biarkan asyncmenetes, membuat tindakan pengontrol Anda asinkron sebagai langkah terakhir.

Dan dalam keadaan apa pun Anda tidak boleh menggunakan Task.Rundalam skenario ini.

Stephen Cleary
sumber
4
Terima kasih Stephen atas komentar berharga Anda. Saya memang mengubah semua metode lapisan layanan saya menjadi async dan sekarang saya memanggil panggilan REST eksternal saya menggunakan metode ExecuteTaskAsync dan bekerja seperti yang diharapkan. Terima kasih juga atas postingan blog Anda tentang asinkron dan Tugas. Itu sangat membantu saya untuk mendapatkan pemahaman awal.
arp
5
Bisakah Anda menjelaskan mengapa Anda tidak boleh menggunakan Task.Run di sini?
Maarten
1
@ Maarten: Saya menjelaskannya (secara singkat) di artikel yang saya tautkan . Saya membahas lebih detail di blog saya .
Stephen Cleary
Saya telah membaca artikel Anda Stephen dan tampaknya menghindari menyebutkan sesuatu. Ketika permintaan ASP.NET tiba dan mulai berjalan di thread pool thread, itu bagus. Tetapi jika menjadi asinkron maka ya itu memulai pemrosesan asinkron dan utas itu segera kembali ke kumpulan. Tetapi pekerjaan yang dilakukan dalam metode async akan dijalankan dengan sendirinya pada thread pool thread!
Hugh
@Hugh: Operasi I / O asinkron tidak memblokir sebuah thread .
Stephen Cleary
12

Itu benar, tapi mungkin tidak berguna.

Karena tidak ada yang perlu ditunggu - tidak ada panggilan untuk memblokir API yang dapat beroperasi secara asinkron - maka Anda menyiapkan struktur untuk melacak operasi asinkron (yang memiliki overhead) tetapi kemudian tidak memanfaatkan kemampuan itu.

Misalnya, jika lapisan layanan melakukan operasi DB dengan Entity Framework yang mendukung panggilan asinkron:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Anda akan mengizinkan utas pekerja untuk melakukan sesuatu yang lain saat db dikueri (dan dengan demikian dapat memproses permintaan lain).

Await cenderung menjadi sesuatu yang perlu dilakukan sepenuhnya: sangat sulit untuk menyesuaikan diri dengan sistem yang ada.

Richard
sumber
-1

Anda tidak memanfaatkan async / await secara efektif karena utas permintaan akan diblokir saat menjalankan sinkronisasi metodeReturnAllCountries()

Utas yang ditugaskan untuk menangani permintaan akan menunggu sebentar ReturnAllCountries() itu berfungsi.

Jika Anda dapat menerapkan ReturnAllCountries()menjadi asinkron, Anda akan melihat manfaat skalabilitas. Ini karena utas dapat dilepaskan kembali ke kumpulan utas .NET untuk menangani permintaan lain, saat ReturnAllCountries()sedang mengeksekusi. Ini akan memungkinkan layanan Anda memiliki throughput yang lebih tinggi, dengan memanfaatkan utas secara lebih efisien.

James Wierzba
sumber
Ini tidak benar. Soket terhubung tetapi thread yang memproses permintaan dapat melakukan sesuatu yang lain, seperti memproses permintaan yang berbeda, sambil menunggu panggilan layanan.
Garr Godfrey
-8

Saya akan mengubah lapisan layanan Anda menjadi:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

seperti yang Anda miliki, Anda masih menjalankan _service.Processpanggilan Anda secara serempak, dan mendapatkan sedikit atau tidak ada manfaat dari menunggunya.

Dengan pendekatan ini, Anda membungkus panggilan yang berpotensi lambat dalam Task, memulainya, dan mengembalikannya untuk ditunggu. Sekarang Anda mendapatkan keuntungan dari menunggu Task.

Jonesopolis
sumber
3
Sesuai tautan blog , saya dapat melihat bahwa meskipun kami menggunakan Task. Jalankan metode ini masih berjalan secara sinkron?
arp
Hmm. Ada banyak perbedaan antara kode di atas dan tautan itu. Anda Servicebukan metode asinkron, dan tidak sedang menunggu dalam Servicepanggilan itu sendiri. Saya bisa mengerti maksudnya, tapi saya tidak percaya itu berlaku di sini.
Jonesopolis
8
Task.Runharus dihindari di ASP.NET. Pendekatan ini akan menghapus semua manfaat dari async/ awaitdan benar-benar berperforma lebih buruk saat dimuat daripada hanya panggilan sinkron.
Stephen Cleary