Artikel ini cukup kuno, tetapi tidak kehilangan relevansinya. Ketika datang ke async / menunggu, tautan ke sana biasanya muncul. Saya tidak dapat menemukan terjemahan ke dalam bahasa Rusia, saya memutuskan untuk membantu seseorang yang tidak lancar.
Pemrograman asinkron telah lama menjadi kerajaan pengembang paling berpengalaman dengan keinginan masokisme - mereka yang memiliki cukup waktu luang, kecenderungan dan kemampuan psikis untuk memikirkan panggilan balik dari panggilan balik dalam aliran eksekusi non-linear. Dengan munculnya Microsoft .NET Framework 4.5, C # dan Visual Basic membuat kita semua tidak sinkron, jadi manusia biasa sekarang dapat menulis metode asinkron hampir semudah yang sinkron. Panggilan balik tidak lagi diperlukan. Tidak ada lagi kode marshaling yang eksplisit dari satu konteks sinkronisasi ke yang lain. Tidak ada lagi kekhawatiran tentang bagaimana hasil eksekusi atau pengecualian bergerak. Tidak perlu trik yang mendistorsi sarana bahasa pemrograman untuk kenyamanan mengembangkan kode asinkron. Singkatnya, tidak ada lagi masalah dan sakit kepala.
Tentu saja, meskipun sekarang mudah untuk mulai menulis metode asinkron (lihat artikel oleh Eric Lippert dan Mads Torgersen di Majalah MSDN ini [OKTOBER 2011] ), diperlukan pemahaman untuk melakukan ini dengan benar. apa yang terjadi di bawah tenda. Setiap kali bahasa atau pustaka meningkatkan tingkat abstraksi yang dapat digunakan pengembang, ini pasti disertai dengan biaya tersembunyi yang mengurangi produktivitas. Dalam banyak kasus, biaya ini dapat diabaikan, sehingga mereka dapat diabaikan dalam kebanyakan kasus oleh sebagian besar programmer. Namun, pengembang yang maju harus sepenuhnya memahami biaya apa yang ada untuk mengambil tindakan yang diperlukan dan menyelesaikan masalah yang mungkin terjadi jika mereka muncul dengan sendirinya. Ini diperlukan saat menggunakan alat pemrograman asinkron dalam C # dan Visual Basic.
Pada artikel ini, saya akan menjelaskan input dan output dari metode asinkron, menjelaskan bagaimana metode asinkron diterapkan, dan membahas beberapa biaya yang lebih kecil. Perhatikan bahwa ini bukan rekomendasi untuk mengubah kode yang dapat dibaca menjadi sesuatu yang sulit dipertahankan, atas nama optimasi dan kinerja mikro. Ini hanya pengetahuan yang akan membantu mendiagnosis masalah yang mungkin Anda temui, dan seperangkat alat untuk mengatasi masalah ini. Selain itu, artikel ini didasarkan pada pratinjau .NET Framework versi 4.5, dan mungkin detail implementasi spesifik dapat berubah pada rilis final.
Dapatkan model berpikir yang nyaman
Selama beberapa dekade, programmer telah menggunakan bahasa pemrograman tingkat tinggi C #, Visual Basic, F # dan C ++ untuk mengembangkan aplikasi yang produktif. Pengalaman ini memungkinkan programmer untuk mengevaluasi biaya berbagai operasi dan mendapatkan pengetahuan tentang teknik pengembangan terbaik. Sebagai contoh, dalam kebanyakan kasus, memanggil metode sinkron relatif ekonomis, terutama jika kompiler dapat menanamkan isi metode dipanggil langsung ke titik panggilan. Oleh karena itu, pengembang terbiasa memecah kode menjadi metode yang kecil dan mudah dirawat, tanpa harus khawatir tentang konsekuensi negatif dari peningkatan jumlah panggilan. Model pemikiran programmer ini dirancang untuk menangani pemanggilan metode.
Dengan munculnya metode asinkron, model pemikiran baru diperlukan. C # dan Visual Basic dengan kompiler mereka dapat membuat ilusi bahwa metode asinkron berfungsi sebagai mitra sinkronnya, meskipun semuanya benar-benar salah di dalam. Kompiler menghasilkan sejumlah besar kode untuk pemrogram, sangat mirip dengan template standar yang ditulis pengembang untuk mendukung asinkron selama waktu yang diperlukan untuk melakukannya dengan tangan. Selain itu, kode yang dihasilkan oleh kompiler berisi panggilan ke fungsi pustaka dari .NET Framework, lebih lanjut mengurangi jumlah pekerjaan yang perlu dilakukan oleh seorang programmer. Untuk memiliki model pemikiran yang tepat dan menggunakannya untuk membuat keputusan yang tepat, penting untuk memahami apa yang dihasilkan oleh kompiler untuk Anda.
Lebih banyak metode, lebih sedikit panggilan
Saat bekerja dengan kode sinkron, menjalankan metode dengan konten kosong praktis tidak berharga. Untuk metode asinkron, ini tidak terjadi. Pertimbangkan metode asinkron ini, yang terdiri dari satu instruksi (dan yang, karena kurangnya pernyataan menunggu, akan dieksekusi secara serempak):
public static async Task SimpleBodyAsync() { Console.WriteLine("Hello, Async World!"); }
Decompiler bahasa perantara (IL) akan mengungkapkan isi sebenarnya dari fungsi ini setelah kompilasi, menghasilkan sesuatu yang mirip dengan Gambar 1. Apa yang merupakan one-liner sederhana diubah menjadi dua metode, salah satunya milik kelas tambahan dari mesin negara. Yang pertama adalah metode rintisan yang memiliki tanda tangan yang mirip dengan yang ditulis oleh programmer (metode ini memiliki nama yang sama, cakupan yang sama, dibutuhkan parameter yang sama dan mengembalikan jenis yang sama), tetapi tidak mengandung kode yang ditulis oleh programmer. Ini hanya berisi pelat standar untuk pengaturan awal. Kode pengaturan awal menginisialisasi mesin negara yang diperlukan untuk mewakili metode asinkron, dan mulai menggunakan panggilan ke metode utilitas MoveNext. Jenis objek mesin negara berisi variabel dengan status eksekusi metode asinkron, yang memungkinkan Anda untuk menyimpannya saat beralih antara titik-titik tunggu asinkron. Ini juga berisi kode yang ditulis oleh seorang programmer, dimodifikasi untuk memastikan transfer hasil eksekusi dan pengecualian ke objek Tugas yang dikembalikan; memegang posisi saat ini dalam metode sehingga eksekusi dapat dilanjutkan dari posisi ini setelah melanjutkan, dll.
Gambar 1 Templat Metode Asinkron
[DebuggerStepThrough] public static Task SimpleBodyAsync() { <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0(); d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.MoveNext(); return d__.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Sequential)] private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public void MoveNext() { try { if (this.<>1__state == -1) return; Console.WriteLine("Hello, Async World!"); } catch (Exception e) { this.<>1__state = -1; this.<>t__builder.SetException(e); return; } this.<>1__state = -1; this.<>t__builder.SetResult(); } ... }
Ketika Anda bertanya-tanya berapa banyak biaya panggilan ke metode asinkron, ingat pola ini. Blok coba / tangkap dalam metode MoveNext diperlukan untuk mencegah kemungkinan upaya untuk menanamkan metode ini oleh JIT oleh kompiler, jadi setidaknya kita mendapatkan biaya pemanggilan metode, sementara saat menggunakan metode sinkron, kemungkinan besar panggilan ini tidak akan (asalkan konten minimalis). Kami akan menerima beberapa panggilan ke prosedur Framework (misalnya, SetResult). Serta beberapa operasi tulis di bidang objek mesin negara. Tentu saja, kita perlu membandingkan semua biaya ini dengan biaya Console.WriteLine, yang mungkin akan menang (mereka termasuk biaya penguncian, I / O, dll.) Perhatikan optimasi yang dibuat lingkungan untuk Anda. Sebagai contoh, objek mesin negara diimplementasikan sebagai struct. Struktur ini akan dikotak dalam tumpukan terkelola hanya jika metode perlu menjeda eksekusi, menunggu operasi selesai, dan ini tidak akan pernah terjadi dalam metode sederhana ini. Jadi pola metode asinkron ini tidak akan membutuhkan alokasi memori dari heap. Compiler dan runtime akan mencoba meminimalkan jumlah operasi alokasi memori.
Ketika tidak menggunakan Async
.NET Framework mencoba menghasilkan implementasi yang efisien untuk metode asinkron menggunakan berbagai metode optimisasi. Namun demikian, pengembang, berdasarkan pengalaman mereka, sering menerapkan metode optimasi mereka, yang dapat berisiko dan tidak praktis untuk otomatisasi oleh kompiler dan runtime, ketika mereka mencoba menggunakan pendekatan universal. Jika Anda tidak melupakan hal ini, penolakan untuk menggunakan metode async bermanfaat dalam sejumlah kasus tertentu, khususnya, ini berlaku untuk metode di perpustakaan yang dapat digunakan dengan pengaturan yang lebih baik. Biasanya ini terjadi ketika diketahui dengan pasti bahwa metode ini dapat dijalankan secara serempak, karena data yang menjadi sandarannya sudah siap.
Saat membuat metode asinkron, pengembang .NET Framework menghabiskan banyak waktu untuk mengoptimalkan jumlah operasi manajemen memori. Ini diperlukan karena manajemen memori menimbulkan biaya terbesar dalam kinerja infrastruktur asinkron. Operasi mengalokasikan memori untuk suatu objek biasanya relatif murah. Mengalokasikan memori untuk objek mirip dengan mengisi keranjang dengan produk di supermarket - Anda tidak menghabiskan apa pun saat Anda memasukkannya ke dalam kereta. Pengeluaran terjadi ketika Anda membayar di kasir, mengeluarkan dompet Anda dan memberikan uang yang layak. Dan jika alokasi memori mudah, pengumpulan sampah selanjutnya dapat sangat memengaruhi kinerja aplikasi. Saat Anda memulai pengumpulan sampah, pemindaian dan penandaan objek yang saat ini berada di memori tetapi tidak memiliki tautan dilakukan. Semakin banyak objek ditempatkan, semakin lama waktu yang dibutuhkan untuk menandainya. Selain itu, semakin besar jumlah benda berukuran besar yang ditempatkan, semakin sering pengumpulan sampah diperlukan. Aspek bekerja dengan memori ini memiliki dampak global pada sistem: semakin banyak sampah yang dihasilkan oleh metode asinkron, semakin lambat aplikasi berjalan, bahkan jika mikrotest tidak menunjukkan biaya yang signifikan.
Untuk metode asinkron yang menunda pelaksanaannya (menunggu data yang belum siap), lingkungan harus membuat objek bertipe Tugas, yang akan dikembalikan dari metode, karena objek ini berfungsi sebagai referensi unik untuk panggilan. Namun, seringkali panggilan metode asinkron dapat dibuat tanpa penangguhan. Kemudian runtime dapat kembali dari cache objek Tugas yang sebelumnya diselesaikan, yang digunakan lagi dan lagi tanpa perlu membuat objek Tugas baru. Benar, ini hanya diperbolehkan dalam kondisi tertentu, misalnya, ketika metode asinkron mengembalikan objek non-universal (non-generik) Tugas, Tugas, atau ketika Tugas universal ditentukan oleh jenis referensi TResult, dan null dikembalikan dari metode. Meskipun daftar kondisi ini berkembang dari waktu ke waktu, masih lebih baik jika Anda tahu bagaimana operasi dilaksanakan.
Pertimbangkan implementasi jenis ini sebagai MemoryStream. MemoryStream diwarisi dari Stream, dan mendefinisikan kembali metode baru yang diimplementasikan dalam .NET 4.5: ReadAsync, WriteAsync dan FlushAsync, untuk menyediakan optimasi kode spesifik memori. Karena operasi baca dilakukan dari buffer yang terletak di memori, yaitu, sebenarnya itu adalah salinan dari area memori, kinerja terbaik adalah jika ReadAsync dijalankan dalam mode sinkron. Implementasi ini dalam metode asinkron mungkin terlihat seperti ini:
public override async Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return this.Read(buffer, offset, count); }
Cukup sederhana. Dan karena Read adalah panggilan sinkron, dan metode ini tidak memiliki pernyataan menunggu untuk mengendalikan harapan, semua panggilan ke ReadAsync ini akan benar-benar dieksekusi secara sinkron. Sekarang mari kita lihat kasus standar menggunakan utas, misalnya, operasi penyalinan:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Harap perhatikan bahwa dalam contoh ReadAsync yang diberikan, aliran sumber selalu dipanggil dengan parameter panjang buffer yang sama, yang berarti sangat mungkin bahwa nilai pengembalian (jumlah byte yang dibaca) juga akan diulang. Kecuali dalam beberapa keadaan yang jarang, penerapan ReadAsync tidak mungkin menggunakan objek Tugas yang di-cache sebagai nilai balik, tetapi Anda bisa melakukannya.
Pertimbangkan opsi implementasi lain untuk metode ini, yang ditunjukkan pada Gambar 2. Menggunakan keunggulan dari aspek yang melekat dalam skrip standar untuk metode ini, kita dapat mengoptimalkan implementasi dengan mengecualikan operasi alokasi memori, yang tidak mungkin diharapkan dari runtime. Kita dapat sepenuhnya menghilangkan kehilangan memori dengan mengembalikan objek Tugas yang sama yang digunakan dalam panggilan ReadAsync sebelumnya jika jumlah byte yang sama terbaca. Dan untuk operasi tingkat rendah seperti itu, yang kemungkinan akan sangat cepat dan akan dipanggil berulang kali, optimasi ini akan memiliki efek yang signifikan, terutama dalam jumlah pengumpulan sampah.
Gambar 2 Optimasi pembuatan tugas
private Task<int> m_lastTask; public override Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource<int>(); tcs.SetCanceled(); return tcs.Task; } try { int numRead = this.Read(buffer, offset, count); return m_lastTask != null && numRead == m_lastTask.Result ? m_lastTask : (m_lastTask = Task.FromResult(numRead)); } catch(Exception e) { var tcs = new TaskCompletionSource<int>(); tcs.SetException(e); return tcs.Task; } }
Metode pengoptimalan yang serupa dengan menghilangkan pembuatan objek Task yang tidak perlu dapat digunakan jika caching diperlukan. Pertimbangkan metode yang dirancang untuk mengambil konten halaman web dan cache untuk referensi di masa mendatang. Sebagai metode asinkron, ini dapat ditulis sebagai berikut (menggunakan perpustakaan System.Net.Http.dll baru untuk .NET 4.5):
private static ConcurrentDictionary<string,string> s_urlToContents; public static async Task<string> GetContentsAsync(string url) { string contents; if (!s_urlToContents.TryGetValue(url, out contents)) { var response = await new HttpClient().GetAsync(url); contents = response.EnsureSuccessStatusCode().Content.ReadAsString(); s_urlToContents.TryAdd(url, contents); } return contents; }
Ini adalah implementasi dahi. Dan untuk panggilan GetContentsAsync yang tidak menemukan data dalam cache, overhead pembuatan objek Tugas baru dapat diabaikan dibandingkan dengan biaya menerima data melalui jaringan. Namun, dalam hal mendapatkan data dari cache, biaya ini menjadi signifikan jika Anda hanya membungkus dan memberikan data lokal yang tersedia.
Untuk menghilangkan biaya ini (jika perlu untuk mencapai kinerja tinggi), Anda dapat menulis ulang metode seperti yang ditunjukkan pada Gambar 3. Sekarang kita memiliki dua metode: metode publik sinkron dan metode pribadi asinkron, yang didelegasikan oleh publik. Koleksi Kamus sekarang menyimpan cache objek Tugas yang dibuat, bukan isinya, sehingga upaya di masa mendatang untuk mengambil konten halaman yang sebelumnya berhasil diperoleh dapat dilakukan dengan hanya mengakses koleksi untuk mengembalikan objek Tugas yang ada. Di dalam, Anda dapat mengambil keuntungan dari menggunakan metode ContinueWith dari objek Task, yang memungkinkan kami untuk menyimpan objek yang dieksekusi dalam koleksi - dalam kasus pemuatan halaman berhasil. Tentu saja, kode ini lebih kompleks dan membutuhkan banyak pengembangan dan dukungan, seperti biasa ketika mengoptimalkan kinerja: Anda tidak ingin menghabiskan waktu menulisnya sampai pengujian kinerja menunjukkan bahwa komplikasi ini mengarah pada peningkatannya, yang mengesankan dan jelas. Perbaikan apa yang sebenarnya akan tergantung pada metode aplikasi. Anda dapat mengambil test suite yang mensimulasikan kasus penggunaan umum dan mengevaluasi hasilnya untuk menentukan apakah game tersebut layak untuk ditiru.
Gambar 3 Tugas cache secara manual
private static ConcurrentDictionary<string,Task<string>> s_urlToContents; public static Task<string> GetContentsAsync(string url) { Task<string> contents; if (!s_urlToContents.TryGetValue(url, out contents)) { contents = GetContentsInternalAsync(url); contents.ContinueWith(delegate { s_urlToContents.TryAdd(url, contents); }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuatOptions.ExecuteSynchronously, TaskScheduler.Default); } return contents; } private static async Task<string> GetContentsInternalAsync(string url) { var response = await new HttpClient().GetAsync(url); return response.EnsureSuccessStatusCode().Content.ReadAsString(); }
Metode optimasi lain yang terkait dengan objek Tugas adalah untuk menentukan apakah akan mengembalikan objek seperti itu dari metode asinkron sama sekali. Baik C # dan Visual Basic mendukung metode asinkron yang mengembalikan nilai nol (kosong), dan mereka tidak membuat objek Tugas sama sekali. Metode asinkron di pustaka harus selalu mengembalikan Tugas dan Tugas, karena ketika mendesain pustaka Anda tidak bisa tahu bahwa mereka tidak akan digunakan menunggu penyelesaian. Namun, ketika mengembangkan aplikasi, metode yang mengembalikan batal dapat menemukan tempatnya. Alasan utama untuk keberadaan metode tersebut adalah untuk menyediakan lingkungan berbasis event yang ada, seperti ASP.NET dan Windows Presentation Foundation (WPF). Menggunakan async dan menunggu, metode ini membuatnya mudah untuk mengimplementasikan penangan tombol, acara pemuatan halaman, dll. Jika Anda bermaksud menggunakan metode asinkron dengan void, berhati-hatilah dengan menangani pengecualian: pengecualian dari itu akan muncul di setiap SynchronizationContext yang aktif pada saat metode dipanggil.
Jangan lupakan konteksnya
Ada banyak konteks berbeda dalam .NET Framework: LogicalCallContext, SynchronizationContext, HostExecutionContext, SecurityContext, ExecutionContext, dan lainnya (jumlah raksasa mereka mungkin menyarankan bahwa pembuat Framework secara finansial termotivasi secara finansial untuk membuat konteks baru, tetapi saya tahu pasti tidak demikian). Beberapa konteks ini sangat memengaruhi metode asinkron, tidak hanya dalam hal fungsionalitas, tetapi juga dalam kinerja.
SynchronizationContext SynchronizationContext memainkan peran penting untuk metode asinkron. "Konteks sinkronisasi" hanyalah abstraksi untuk memastikan bahwa permintaan delegasi dengan spesifikasi perpustakaan atau lingkungan tertentu dikerahkan. Misalnya, WPF memiliki DispatcherSynchronizationContext untuk mewakili utas antarmuka pengguna (UI) untuk Dispatcher: mengirim delegasi ke konteks sinkronisasi ini menyebabkan delegasi ini harus antri untuk dieksekusi oleh Dispatcher di utasnya. ASP.NET menyediakan AspNetSynchronizationContext yang digunakan untuk memastikan bahwa operasi asinkron yang terlibat dalam memproses permintaan ASP.NET dijamin akan dieksekusi secara berurutan dan terikat pada keadaan HttpContext yang benar. Baik, dll Secara umum, ada sekitar 10 spesialisasi SynchronizationContext di .NET Framework, beberapa terbuka, beberapa internal.
Saat menunggu Tugas atau objek dari jenis lain yang .NET Framework dapat mengimplementasikannya, objek yang menunggu (misalnya, TaskAwaiter) menangkap Konteks Sinkronisasi saat ini pada saat penantian (tunggu) dimulai. Setelah menunggu, jika SynchronizationContext ditangkap, kelanjutan metode asinkron dikirim ke konteks sinkronisasi ini. Karena itu, pemrogram yang menulis metode asinkron yang dipanggil dari aliran UI tidak perlu secara manual memanggil kembali ke aliran UI untuk memperbarui kontrol UI: Kerangka melakukan marshaling ini secara otomatis.
Sayangnya, marsaling ini ada harganya. Untuk pengembang aplikasi yang menggunakan menunggu untuk menerapkan aliran kontrol mereka, marshaling otomatis adalah solusi yang tepat. Perpustakaan sering memiliki cerita yang sangat berbeda. Untuk pengembang aplikasi, marshaling ini terutama diperlukan untuk kode untuk mengontrol konteks di mana ia dijalankan, misalnya, untuk mengakses kontrol UI atau untuk mengakses HttpContext sesuai dengan permintaan ASP.NET yang diperlukan. Namun, perpustakaan umumnya tidak diharuskan untuk memenuhi persyaratan seperti itu. Akibatnya, marsaling otomatis seringkali membawa biaya tambahan yang sama sekali tidak perlu. Mari kita lihat lagi kode yang menyalin data dari satu aliran ke yang lain:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Jika salinan ini dipanggil dari aliran UI, setiap operasi baca dan tulis akan memaksa eksekusi untuk kembali ke aliran UI. Dalam kasus megabyte data di sumber dan aliran yang membaca dan menulis secara tidak sinkron (yaitu, sebagian besar implementasinya), ini berarti sekitar 500 switch dari aliran latar belakang ke aliran UI. Untuk menangani perilaku ini di jenis Tugas dan Tugas, metode ConfigureAwait dibuat. Metode ini menerima parameter continueOnCapturedContext dari tipe boolean yang mengontrol marshaling. Jika benar (default), tunggu secara otomatis mengembalikan kontrol ke Konteks Synchronization. Jika salah digunakan, konteks sinkronisasi akan diabaikan, dan lingkungan akan terus menjalankan operasi asinkron di utas tempat terputus. Menerapkan logika ini akan memberikan versi yang lebih efisien dari kode copy antar utas:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
Untuk pengembang perpustakaan, akselerasi seperti itu sendiri sudah cukup untuk selalu berpikir tentang menggunakan ConfigureAwait, dengan pengecualian kondisi langka di mana perpustakaan cukup tahu tentang runtime dan perlu menjalankan metode dengan akses ke konteks yang benar.
Selain kinerja, ada alasan lain bahwa Anda perlu menggunakan ConfigureAwait saat mengembangkan perpustakaan. Bayangkan bahwa metode CopyStreamToStreamAsync, diimplementasikan dengan versi kode tanpa ConfigureAwait, dipanggil dari aliran UI di WPF, misalnya, seperti ini:
private void button1_Click(object sender, EventArgs args) { Stream src = β¦, dst = β¦; Task t = CopyStreamToStreamAsync(src, dst); t.Wait(); // deadlock! }
Dalam hal ini, programmer harus menulis button1_Click sebagai metode asinkron di mana operator menunggu diharapkan untuk mengeksekusi Tugas, dan tidak menggunakan metode Tunggu sinkron objek ini. Metode Tunggu perlu digunakan dalam banyak kasus lain, tetapi hampir selalu merupakan kesalahan untuk menggunakannya menunggu di aliran UI, seperti yang ditunjukkan di sini. Metode Tunggu tidak akan kembali sampai Tugas selesai. Dalam kasus CopyStreamToStreamAsync, aliran asinkronnya mencoba mengembalikan eksekusi dengan mengirim data ke Konteks Synchronization yang diambil, dan tidak dapat menyelesaikan sampai transfer tersebut selesai (karena mereka perlu melanjutkan operasinya). Tetapi kiriman ini, pada gilirannya, tidak dapat dieksekusi, karena utas UI yang harus menanganinya diblokir oleh panggilan Tunggu. Ini adalah ketergantungan siklus yang menyebabkan kebuntuan. Jika CopyStreamToStreamAsync diimplementasikan dengan ConfigureAwait (false), tidak akan ada ketergantungan dan pemblokiran.
ExecutionContext ExecutionContext adalah bagian penting dari .NET Framework, tetapi masih sebagian besar programmer tidak menyadari keberadaannya. ExecutionContext β , SecurityContext LogicalCallContext, , . , ThreadPool.QueueUserWorkItem, Task.Run, Delegate.BeginInvoke, Stream.BeginRead, WebClient.DownloadStringAsync Framework, ExecutionContext ExecutionContext.Run ( ). , , ThreadPool.QueueUserWorkItem, Windows (identity), WaitCallback. , Task.Run LogicalCallContext, LogicalCallContext Action. ExecutionContext .
Framework , ExecutionContext, , . Windows LogicalCallContext . (WindowsIdentity.Impersonate CallContext.LogicalSetData) .
. C# Visual Basic , . await. , , - . C# Visual Basic («») , await (boxed) , .
. , . , , , .
C# Visual Basic , . ,
public static async Task FooAsync() { var dto = DateTimeOffset.Now; var dt = dto.DateTime; await Task.Yield(); Console.WriteLine(dt); }
dto await, . , , - dto:
Figure 4
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public DateTimeOffset <dto>5__1; public DateTime <dt>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
, . , , , , . , :
public static async Task FooAsync() { var dt = DateTimeOffset.Now.DateTime; await Task.Yield(); Console.WriteLine(dt); }
, .NET (GC) , , , : 0, , , (.NET GC 0, 1 2). , GC . , , , , , , . 0, , , . , , , .
( , ). JIT , , , , . , , . , , , , . , , . , C# Visual Basic , , .
C# Visual Basic , awaits: . await , Task , , . , , :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return Sum(await a, await b, await c); } private static int Sum(int a, int b, int c) { return a + b + c; }
C# βawait bβ Sum. await, Sum, - async , «» await. , await . , , CLR, , , . , <>t__stack. , , Tuple<int, int> <>__stack. , , , . , SumAsync :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int ra = await a; int rb = await b; int rc = await c; return Sum(ra, rb, rc); }
, ra, rb rc, . , : . , , , . , , , , .
, , . Sum , await , . , await , . await , Task.WhenAll:
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int [] results = await Task.WhenAll(a, b, c); return Sum(results[0], results[1], results[2]); }
Task.WhenAll Task<TResult[]>, , , , . . , WhenAll, Task Task. , , , , , WhenAll , . WhenAll, , , params, . , , . Figure 5
Figure 5
public static Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return (a.Status == TaskStatus.RanToCompletion && b.Status == TaskStatus.RanToCompletion && c.Status == TaskStatus.RanToCompletion) ? Task.FromResult(Sum(a.Result, b.Result, c.Result)) : SumAsyncInternal(a, b, c); } private static async Task<int> SumAsyncInternal(Task<int> a, Task<int> b, Task<int> c) { await Task.WhenAll((Task)a, b, c).ConfigureAwait(false); return Sum(a.Result, b.Result, c.Result); }
, . , . , . , , : , , / , . .NET Framework , . , .NET Framework, . , , Framework, , , .