Membungkus kode sinkron menjadi panggilan asinkron

97

Saya memiliki metode dalam aplikasi ASP.NET, yang menghabiskan cukup banyak waktu untuk menyelesaikannya. Panggilan ke metode ini mungkin terjadi hingga 3 kali selama satu permintaan pengguna, bergantung pada status cache dan parameter yang disediakan pengguna. Setiap panggilan membutuhkan waktu sekitar 1-2 detik. Metodenya sendiri adalah panggilan sinkron ke layanan dan tidak ada kemungkinan untuk mengganti implementasi.
Jadi panggilan sinkron ke layanan terlihat seperti berikut:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

Dan penggunaan metode ini adalah (perhatikan, pemanggilan metode dapat terjadi lebih dari sekali):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Saya mencoba mengubah implementasi dari sisi saya untuk memberikan pekerjaan simultan dari metode ini, dan inilah yang saya sampai sejauh ini.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Penggunaan (bagian dari kode "lakukan hal lain" berjalan bersamaan dengan panggilan ke layanan):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Pertanyaan saya adalah sebagai berikut. Apakah saya menggunakan pendekatan yang tepat untuk mempercepat eksekusi di aplikasi ASP.NET atau saya melakukan pekerjaan yang tidak perlu mencoba menjalankan kode sinkron secara asinkron? Adakah yang bisa menjelaskan mengapa pendekatan kedua bukanlah pilihan di ASP.NET (jika memang tidak)? Juga, jika pendekatan seperti itu dapat diterapkan, apakah saya perlu memanggil metode tersebut secara asinkron jika itu adalah satu-satunya panggilan yang mungkin kami lakukan saat ini (saya punya kasus seperti itu, ketika tidak ada hal lain yang harus dilakukan sambil menunggu penyelesaian)?
Sebagian besar artikel di internet tentang topik ini mencakup penggunaan async-awaitpendekatan dengan kode, yang sudah menyediakan awaitablemetode, tetapi itu bukan kasus saya. Siniadalah artikel bagus yang menjelaskan kasus saya, yang tidak menjelaskan situasi panggilan paralel, menolak opsi untuk membungkus panggilan sinkronisasi, tetapi menurut pendapat saya situasi saya adalah kesempatan untuk melakukannya.
Terima kasih sebelumnya atas bantuan dan tip.

Eadel
sumber

Jawaban:

119

Penting untuk membuat perbedaan antara dua jenis konkurensi yang berbeda. Konkurensi asinkron adalah saat Anda memiliki beberapa operasi asinkron dalam penerbangan (dan karena setiap operasi asinkron, tidak ada yang benar-benar menggunakan utas ). ParalelKonkurensi adalah ketika Anda memiliki beberapa utas yang masing-masing melakukan operasi terpisah.

Hal pertama yang harus dilakukan adalah mengevaluasi kembali asumsi ini:

Metodenya sendiri adalah panggilan sinkron ke layanan dan tidak ada kemungkinan untuk mengganti implementasi.

Jika "layanan" Anda adalah layanan web atau apa pun yang terikat I / O, solusi terbaik adalah menulis API asinkron untuknya.

Saya akan melanjutkan dengan asumsi bahwa "layanan" Anda adalah operasi terikat CPU yang harus dijalankan pada mesin yang sama dengan server web.

Jika demikian, maka hal berikutnya yang perlu dievaluasi adalah asumsi lain:

Saya butuh permintaan untuk mengeksekusi lebih cepat.

Apakah Anda benar-benar yakin itu yang perlu Anda lakukan? Apakah ada perubahan front-end yang dapat Anda buat - misalnya, memulai permintaan dan mengizinkan pengguna melakukan pekerjaan lain saat sedang diproses?

Saya akan melanjutkan dengan asumsi bahwa ya, Anda benar-benar perlu membuat permintaan individu dieksekusi lebih cepat.

Dalam kasus ini, Anda harus menjalankan kode paralel di server web Anda. Ini jelas tidak disarankan secara umum karena kode paralel akan menggunakan utas yang ASP.NET mungkin perlu menangani permintaan lain, dan dengan menghapus / menambahkan utas itu akan membuang heuristik threadpool ASP.NET off. Jadi, keputusan ini berdampak pada seluruh server Anda.

Saat Anda menggunakan kode paralel di ASP.NET, Anda membuat keputusan untuk benar-benar membatasi skalabilitas aplikasi web Anda. Anda juga mungkin melihat cukup banyak thread churn, terutama jika permintaan Anda meledak sama sekali. Saya merekomendasikan hanya menggunakan kode paralel pada ASP.NET jika Anda tahu bahwa jumlah pengguna secara bersamaan akan cukup rendah (yaitu, bukan server publik).

Jadi, jika Anda sampai sejauh ini, dan Anda yakin ingin melakukan pemrosesan paralel pada ASP.NET, Anda memiliki beberapa opsi.

Salah satu metode yang lebih mudah adalah dengan menggunakan Task.Run, sangat mirip dengan kode Anda yang sudah ada. Namun, saya tidak menyarankan menerapkan CalculateAsyncmetode karena itu menyiratkan pemrosesan asynchronous (yang sebenarnya tidak). Sebagai gantinya, gunakan Task.Runsaat menelepon:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Atau, jika bekerja dengan baik dengan kode Anda, Anda dapat menggunakan Paralleljenis, yaitu, Parallel.For, Parallel.ForEach, atau Parallel.Invoke. Keuntungan dari Parallelkode ini adalah bahwa utas permintaan digunakan sebagai salah satu utas paralel, dan kemudian melanjutkan eksekusi dalam konteks utas (ada lebih sedikit pengalihan konteks daripada asynccontoh):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Saya tidak merekomendasikan penggunaan Parallel LINQ (PLINQ) di ASP.NET sama sekali.

Stephen Cleary
sumber
1
Terima kasih banyak atas jawaban yang mendetail. Satu hal lagi yang ingin saya klarifikasi. Saat saya mulai mengeksekusi menggunakan Task.Run, konten dijalankan di utas lain, yang diambil dari kumpulan utas. Maka tidak perlu sama sekali untuk membungkus panggilan pemblokiran (seperti panggilan tambang ke layanan) ke dalam Runs, karena mereka akan selalu menggunakan satu utas masing-masing, yang akan diblokir selama eksekusi metode. Dalam situasi seperti itu, satu-satunya manfaat yang tersisa dari async-awaitkasus saya adalah melakukan beberapa tindakan sekaligus. Tolong, perbaiki saya jika saya salah.
Eadel
2
Ya, satu-satunya manfaat yang Anda dapatkan dari Run(atau Parallel) adalah konkurensi. Operasi masing-masing masih memblokir utas. Karena Anda mengatakan layanan tersebut adalah layanan web, maka saya tidak menyarankan penggunaan Runatau Parallel; sebagai gantinya, tulis API asinkron untuk layanan tersebut.
Stephen Cleary
3
@Tokopedia apa yang Anda maksud dengan "... dan karena setiap operasi tidak sinkron, tidak ada yang benar-benar menggunakan utas ..." terima kasih!
alltej
3
@alltej: Untuk kode yang benar-benar asinkron, tidak ada utas .
Stephen Cleary
4
@JoshuaFrank: Tidak secara langsung. Kode di belakang API sinkron harus memblokir utas panggilan, menurut definisi. Anda dapat menggunakan API asinkron seperti BeginGetResponsedan memulai beberapa dari itu, dan kemudian (secara sinkron) memblokir sampai semuanya selesai, tetapi pendekatan semacam ini rumit - lihat blog.stephencleary.com/2012/07/dont-block-on -async-code.html dan msdn.microsoft.com/en-us/magazine/mt238404.aspx . Biasanya lebih mudah dan lebih bersih untuk mengadopsi asyncsemua cara, jika memungkinkan.
Stephen Cleary
1

Saya menemukan bahwa kode berikut dapat mengonversi Tugas untuk selalu berjalan secara tidak sinkron

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

dan saya telah menggunakannya dengan cara berikut

await ForceAsync(() => AsyncTaskWithNoAwaits())

Ini akan menjalankan Tugas apa pun secara asinkron sehingga Anda dapat menggabungkannya dalam skenario WhenAll, WhenAny, dan penggunaan lainnya.

Anda juga bisa menambahkan Task.Yield () sebagai baris pertama dari kode yang Anda panggil.

kernowcode.dll
sumber