Dalam praktik saya, saya sering melihat, di lingkungan yang berbeda , kode seperti di bawah ini:
[1] var x = FooWithResultAsync(/*...*/).Result; // [2] FooAsync(/*...*/).Wait(); // [3] FooAsync(/*...*/).GetAwaiter().GetResult(); // [4] FooAsync(/*...*/) .ConfigureAwait(false) .GetAwaiter() .GetResult(); // [5] await FooAsync(/*...*/).ConfigureAwait(false) // [6] await FooAsync(/*...*/)
Dari komunikasi dengan penulis garis-garis tersebut, menjadi jelas bahwa mereka semua dibagi menjadi tiga kelompok:
- Grup pertama adalah mereka yang tidak tahu apa-apa tentang kemungkinan masalah dengan memanggil
Result/Wait/GetResult
. Contoh (1-3) dan kadang-kadang (6) adalah tipikal untuk programmer dari grup ini; - Kelompok kedua termasuk programmer yang menyadari kemungkinan masalah, tetapi mereka tidak tahu penyebab terjadinya mereka. Pengembang dari grup ini, di satu sisi, mencoba menghindari garis seperti (1-3 dan 6), tetapi, di sisi lain, kode penyalahgunaan seperti (4-5);
- Kelompok ketiga, dalam pengalaman saya yang terkecil, adalah para programmer yang tahu bagaimana kode (1-6) bekerja, dan karenanya, dapat membuat pilihan berdasarkan informasi.
Apakah risikonya memungkinkan, dan seberapa besar itu, ketika menggunakan kode, seperti dalam contoh di atas, tergantung, seperti yang saya sebutkan sebelumnya, pada lingkungan .

Risiko dan penyebabnya
Contoh (1-6) dibagi menjadi dua kelompok. Grup pertama adalah kode yang memblokir utas panggilan. Grup ini termasuk (1-4).
Memblokir utas seringkali merupakan ide yang buruk. Mengapa Untuk kesederhanaan, kami mengasumsikan bahwa semua utas dialokasikan dari beberapa kumpulan utas. Jika program memiliki kunci, maka ini dapat menyebabkan pemilihan semua utas dari kolam. Dalam kasus terbaik, ini akan memperlambat program dan menyebabkan penggunaan sumber daya yang tidak efisien. Dalam kasus terburuk, ini dapat menyebabkan kebuntuan, ketika utas tambahan diperlukan untuk menyelesaikan beberapa tugas, tetapi kumpulan tidak dapat mengalokasikannya.
Jadi, ketika pengembang menulis kode seperti (1-4), ia harus berpikir tentang seberapa besar kemungkinan situasi yang dijelaskan di atas.
Tetapi banyak hal menjadi lebih buruk ketika kita bekerja di lingkungan di mana ada konteks sinkronisasi yang berbeda dari standar. Jika ada konteks sinkronisasi khusus , memblokir utas panggilan meningkatkan kemungkinan kebuntuan yang terjadi berkali-kali. Jadi, kode dari contoh (1-3), jika dijalankan di utas WinForms UI, hampir dijamin akan membuat jalan buntu. Saya menulis "praktis" karena ada pilihan saat ini tidak demikian, tetapi lebih pada nanti. Menambahkan ConfigureAwait(false)
, seperti pada (4), tidak akan memberikan 100% jaminan perlindungan terhadap kebuntuan. Berikut ini adalah contoh untuk mengonfirmasi ini:
[7] // / . async Task FooAsync() { // Delay . . await Task.Delay(5000); // RestPartOfMethodCode(); } // "" , , WinForms . private void button1_Click(object sender, EventArgs e) { FooAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); button1.Text = "new text"; }
Artikel "Komputasi Paralel - Semuanya Tentang SynchronizationContext" memberikan informasi tentang berbagai konteks sinkronisasi.
Untuk memahami penyebab kebuntuan, Anda perlu menganalisis kode mesin negara ke mana panggilan ke metode async dikonversi, dan kemudian kode kelas MS. Async Menunggu dan artikel StateMachine Generated memberikan contoh mesin negara tersebut.
Saya tidak akan memberikan kode sumber lengkap yang dihasilkan misalnya (7), automaton, saya hanya akan menunjukkan baris-baris penting untuk analisis lebih lanjut:
// MoveNext. //... // taskAwaiter . taskAwaiter = Task.Delay(5000).GetAwaiter(); if(tasAwaiter.IsCompleted != true) { _awaiter = taskAwaiter; _nextState = ...; _builder.AwaitUnsafeOnCompleted<TaskAwaiter, ThisStateMachine>(ref taskAwaiter, ref this); return; }
Cabang if
dijalankan jika panggilan asinkron ( Delay
) belum selesai dan, oleh karena itu, utas saat ini dapat dibebaskan.
Harap dicatat bahwa di AwaitUnsafeOnCompleted
, taskAwaiter diterima dari panggilan asinkron (relatif ke FooAsync
) internal ( Delay
).
Jika Anda terjun ke hutan sumber MS yang tersembunyi di balik panggilan AwaitUnsafeOnCompleted
, maka, pada akhirnya, kami akan datang ke kelas SynchronizationContextAwaitTaskContinuation , dan kelas dasarnya AwaitTaskContinuation , di mana jawaban untuk pertanyaan itu berada.
Kode kelas-kelas ini dan yang terkait agak membingungkan, oleh karena itu, untuk memfasilitasi persepsi, saya membiarkan diri saya menulis "analog" yang sangat disederhanakan dari contoh apa (7) berubah menjadi, tetapi tanpa mesin negara, dan dalam hal TPL:
[8] Task FooAsync() { // methodCompleted , , // , " ". // , methodCompleted.WaitOne() , // SetResult AsyncTaskMethodBuilder, // . var methodCompleted = new AutoResetEvent(false); SynchronizationContext current = SynchronizationContext.Current; return Task.Delay(5000).ContinueWith( t=> { if(current == null) { RestPartOfMethodCode(methodCompleted); } else { current.Post(state=>RestPartOfMethodCode(methodCompleted), null); methodCompleted.WaitOne(); } }, TaskScheduler.Current); } // // void RestPartOfMethodCode(AutoResetEvent methodCompleted) // { // FooAsync. // methodCompleted.Set(); // }
Dalam contoh (8), penting untuk memperhatikan fakta bahwa jika ada konteks sinkronisasi, semua kode metode asinkron yang muncul setelah penyelesaian panggilan asinkron internal dijalankan melalui konteks ini (panggil saat ini. current.Post(...)
). Fakta ini adalah penyebab kebuntuan. Misalnya, jika kita berbicara tentang aplikasi WinForms, maka konteks sinkronisasi di dalamnya dikaitkan dengan aliran UI. Jika utas UI diblokir, misalnya (7) ini terjadi melalui panggilan ke .GetResult()
, maka sisa kode metode asinkron tidak dapat dieksekusi, yang berarti bahwa metode asinkron tidak dapat menyelesaikan, dan tidak dapat melepaskan utas UI, yang merupakan jalan buntu.
Dalam contoh (7), panggilan ke FooAsync
dikonfigurasi melalui ConfigureAwait(false)
, tetapi ini tidak membantu. Faktanya adalah bahwa Anda perlu mengkonfigurasi objek tunggu yang akan diteruskan ke AwaitUnsafeOnCompleted
, dalam contoh kami, ini adalah objek tunggu dari panggilan Delay
. Dengan kata lain, dalam hal ini, memanggil ConfigureAwait(false)
dalam kode klien tidak masuk akal. Anda dapat memecahkan masalah jika pengembang metode FooAsync
mengubahnya sebagai berikut:
[9] async Task FooAsync() { await Task.Delay(5000).ConfigureAwait(false); // RestPartOfMethodCode(); } private void button1_Click(object sender, EventArgs e) { FooAsync().GetAwaiter().GetResult(); button1.Text = "new text"; }
Di atas, kami memeriksa risiko yang muncul dengan kode kelompok pertama - kode dengan pemblokiran (contoh 1-4). Sekarang tentang grup kedua (contoh 5 dan 6) - kode tanpa kunci. Dalam hal ini, pertanyaannya adalah, kapan panggilan ke ConfigureAwait(false)
dibenarkan? Ketika mem-parsing contoh (7), kami telah menemukan bahwa kami perlu mengonfigurasi objek yang menunggu atas dasar mana kelanjutan dari eksekusi akan dibangun. Yaitu konfigurasi diperlukan (jika Anda mengambil keputusan ini) hanya untuk panggilan asinkron internal .
Siapa yang harus disalahkan?
Seperti biasa, jawaban yang benar adalah "segalanya." Mari kita mulai dengan programmer dari MS. Di satu sisi, pengembang Microsoft memutuskan bahwa, di hadapan konteks sinkronisasi, pekerjaan harus dilakukan melaluinya. Dan ini logis, kalau tidak mengapa masih diperlukan. Dan, seperti yang saya percaya, mereka berharap bahwa pengembang kode "klien" tidak akan memblokir utas, terutama jika konteks sinkronisasi terkait dengannya. Di sisi lain, mereka memberikan alat yang sangat sederhana untuk "menembak diri sendiri di kaki" - itu terlalu sederhana dan nyaman untuk mendapatkan hasilnya melalui pemblokiran. .Result/.GetResult
, atau blok sungai, menunggu panggilan untuk mengakhiri, melalui. .Wait
. Yaitu Pengembang MS telah memungkinkan bahwa penggunaan perpustakaan mereka yang "salah" (atau berbahaya) tidak menyebabkan kesulitan apa pun.
Tetapi ada juga kesalahan pada pengembang kode "klien". Terdiri dari fakta bahwa, seringkali, pengembang tidak mencoba untuk memahami alat mereka dan mengabaikan peringatan. Dan ini adalah jalan langsung menuju kesalahan.
Apa yang harus dilakukan
Di bawah ini saya berikan rekomendasi saya.
Untuk pengembang kode klien
- Lakukan yang terbaik untuk menghindari pemblokiran. Dengan kata lain, jangan campur kode sinkron dan asinkron tanpa kebutuhan khusus.
- Jika Anda harus melakukan kunci, maka tentukan di lingkungan mana kode dieksekusi:
- Apakah ada konteks sinkronisasi? Jika demikian, yang mana? Fitur apa yang ia buat dalam karyanya?
- Jika tidak ada konteks sinkronisasi, maka: Apa yang akan menjadi beban? Apa kemungkinan blok Anda akan menyebabkan "kebocoran" benang dari kolam? Apakah jumlah utas yang dibuat pada awal sudah cukup secara default, atau haruskah saya mengalokasikan lebih banyak?
- Jika kodenya asinkron, apakah Anda perlu mengonfigurasi panggilan asinkron melalui
ConfigureAwait
?
Buat keputusan berdasarkan semua informasi yang diterima. Anda mungkin perlu memikirkan kembali pendekatan implementasi Anda. Mungkin ConfigureAwait
akan membantu Anda, atau mungkin Anda tidak membutuhkannya.
Untuk pengembang perpustakaan
- Jika Anda yakin bahwa kode Anda dapat dipanggil dari "sinkron", maka pastikan untuk menerapkan API sinkron. Itu harus benar-benar sinkron, mis. Anda harus menggunakan API sinkron dari perpustakaan pihak ketiga.
ConfigureAwait(true / false)
.
Di sini, dari sudut pandang saya, diperlukan pendekatan yang lebih halus daripada yang biasanya direkomendasikan. Banyak artikel mengatakan bahwa dalam kode pustaka, semua panggilan asinkron harus dikonfigurasikan melalui ConfigureAwait(false)
. Saya tidak bisa setuju dengan itu. Mungkin, dari sudut pandang penulis, kolega dari Microsoft membuat keputusan yang salah ketika memilih perilaku "default" dalam kaitannya dengan bekerja dengan konteks sinkronisasi. Tetapi mereka (MS), bagaimanapun, meninggalkan kesempatan bagi pengembang kode "klien" untuk mengubah perilaku ini. Strategi, ketika kode pustaka sepenuhnya tercakup oleh ConfigureAwait(false)
, mengubah perilaku default, dan, yang lebih penting, pendekatan ini menghilangkan pengembang kode pilihan "klien".
Opsi saya adalah, ketika menerapkan API asinkron, tambahkan dua parameter input tambahan untuk setiap metode API: CancellationToken token
bool continueOnCapturedContext
dan bool continueOnCapturedContext
. Dan implementasikan kodenya sebagai berikut:
public async Task<string> FooAsync( /* */, CancellationToken token, bool continueOnCapturedContext) { // ... await Task.Delay(30, token).ConfigureAwait(continueOnCapturedContext); // ... return result; }
Parameter pertama, token
, berfungsi, seperti yang Anda ketahui, untuk kemungkinan pembatalan terkoordinasi (pengembang perpustakaan terkadang mengabaikan fitur ini). Yang kedua, continueOnCapturedContext
- memungkinkan Anda mengkonfigurasi interaksi dengan konteks sinkronisasi panggilan asinkron internal.
Pada saat yang sama, jika metode API asinkron sendiri merupakan bagian dari metode asinkron lain, kode "klien" akan dapat menentukan bagaimana ia harus berinteraksi dengan konteks sinkronisasi:
// : async Task ClientFoo() { // "" ClientFoo , // FooAsync . await FooAsync( /* */, ancellationToken.None, false); // . await FooAsync( /* */, ancellationToken.None, false).ConfigureAwait(false); //... } // , . private void button1_Click(object sender, EventArgs e) { FooAsync( /* */, _source.Token, false).GetAwaiter().GetResult(); button1.Text = "new text"; }
Kesimpulannya
Kesimpulan utama dari hal tersebut di atas adalah tiga pemikiran berikut:
- Kunci seringkali merupakan akar dari semua kejahatan. Kehadiran kunci yang dapat menyebabkan, dalam kasus terbaik, degradasi kinerja dan penggunaan sumber daya yang tidak efisien, dalam kondisi terburuk - hingga menemui jalan buntu. Sebelum Anda menggunakan kunci, pertimbangkan apakah ini perlu? Mungkin ada cara sinkronisasi lain yang dapat diterima dalam kasus Anda;
- Pelajari alat yang Anda gunakan;
- Jika Anda mendesain perpustakaan, cobalah untuk memastikan bahwa penggunaannya yang benar mudah, hampir intuitif, dan yang salah penuh dengan kompleksitas.
Saya mencoba sesederhana mungkin untuk menjelaskan risiko yang terkait dengan async / menunggu, dan alasan terjadinya. Dan juga, mempresentasikan visi saya untuk menyelesaikan masalah ini. Saya berharap bahwa saya berhasil, dan materi akan bermanfaat bagi pembaca. Untuk lebih memahami bagaimana segala sesuatu bekerja, Anda harus, tentu saja, merujuk pada sumbernya. Ini dapat dilakukan melalui repositori MS di GitHub atau, bahkan lebih nyaman, melalui situs web MS itu sendiri.
PS Saya akan berterima kasih atas kritik yang membangun.
