.NET: Alat untuk bekerja dengan multithreading dan asynchrony. Bagian 1

Saya menerbitkan artikel asli tentang Habr, yang terjemahannya diposting di blog Codingsight .
Bagian kedua tersedia di sini.

Kebutuhan untuk melakukan sesuatu secara serempak, tanpa menunggu hasilnya di sini dan sekarang, atau untuk berbagi banyak pekerjaan antara beberapa unit yang melakukan itu, bahkan sebelum munculnya komputer. Dengan penampilan mereka, kebutuhan seperti itu menjadi sangat nyata. Sekarang, pada tahun 2019, mengetik artikel ini di laptop dengan prosesor Intel Core 8-core, di mana tidak seratus proses bekerja pada saat yang sama, tetapi lebih banyak utas. Di sebelahnya terdapat ponsel yang sedikit usang, dibeli beberapa tahun yang lalu, dengan prosesor 8-core. Sumber daya tematik penuh dengan artikel dan video di mana penulisnya mengagumi smartphone andalannya tahun ini di mana mereka menempatkan prosesor 16-inti. Kurang dari $ 20 / jam, MS Azure menyediakan mesin virtual dengan 128 prosesor inti dan 2 TB TB. Sayangnya, tidak mungkin memaksimalkan dan mengekang kekuatan ini tanpa bisa mengendalikan interaksi arus.

Terminologi


Proses - Objek OS, ruang alamat terisolasi, berisi utas.
Thread (Utas) - objek OS, unit eksekusi terkecil, bagian dari suatu proses, utas berbagi memori dan sumber daya lainnya di antara mereka sendiri dalam proses.
Multitasking adalah fitur OS, kemampuan untuk menjalankan banyak proses pada saat yang bersamaan
Multicore - properti prosesor, kemampuan untuk menggunakan banyak core untuk pemrosesan data
Multiprocessing - properti komputer, kemampuan untuk bekerja secara bersamaan dengan beberapa prosesor secara fisik
Multithreading adalah properti dari suatu proses, kemampuan untuk mendistribusikan pemrosesan data antara banyak utas.
Paralelisme - melakukan beberapa tindakan secara fisik pada waktu yang sama per unit waktu
Asynchrony - pelaksanaan operasi tanpa menunggu akhir dari pemrosesan ini, hasil dari eksekusi dapat diproses nanti.

Metafora


Tidak semua definisi baik dan beberapa memerlukan penjelasan tambahan, jadi saya akan menambahkan metafora untuk memasak sarapan ke terminologi yang diperkenalkan secara formal. Memasak sarapan dalam metafora ini adalah suatu proses.

Memasak sarapan di pagi hari saya ( CPU ) datang ke dapur ( Komputer ). Saya punya 2 tangan ( Cores ). Dapur memiliki sejumlah perangkat ( IO ): oven, ketel, pemanggang roti, lemari es. Saya menyalakan gas, meletakkan wajan di atasnya dan menuangkan minyak di sana, tanpa menunggu sampai memanas ( asynchronous, Non-Blocking-IO-Wait ), saya mengambil telur dari lemari es dan memecahnya menjadi piring, dan kemudian memukulnya dengan satu tangan ( Thread # 1) ), dan yang kedua ( Thread # 2 ) Saya memegang piring (Sumber Daya Bersama). Sekarang saya masih akan menyalakan ketel, tetapi tidak ada cukup tangan ( Thread Starvation ) Selama waktu ini, penggorengan dipanaskan (Memproses hasilnya) di mana saya menuangkan apa yang saya kocok. Saya meraih ketel dan menyalakannya dan secara bodoh melihat bagaimana air di dalamnya mendidih ( Memblokir-IO-Tunggu ), meskipun saya bisa mencuci piring selama waktu ini, di mana saya mengalahkan telur dadar.

Saya memasak telur dadar hanya dengan 2 tangan, dan saya tidak punya lebih, tetapi pada saat yang sama, 3 operasi terjadi pada saat mengocok telur dadar: mengocok telur dadar, memegang piring, memanaskan wajan. CPU adalah bagian tercepat dari komputer, IO adalah yang lebih sering dari komputer memperlambat segalanya, sehingga seringkali solusi yang efektif adalah mengambil sesuatu CPU saat menerima data dari IO.

Melanjutkan metafora:

  • Jika dalam proses menyiapkan telur dadar, saya juga akan mencoba berganti pakaian, ini akan menjadi contoh multitasking. Nuansa penting: komputer dengan ini jauh lebih baik daripada orang.
  • Dapur dengan beberapa koki, misalnya di restoran, adalah komputer multi-core.
  • Banyak restoran food court di pusat perbelanjaan - pusat data

.NET Tools


Dalam bekerja dengan utas, seperti dalam banyak hal lainnya, .NET bagus. Dengan setiap versi baru, ia menyajikan semakin banyak alat baru untuk bekerja dengannya, lapisan abstraksi baru di atas utas OS. Dalam bekerja dengan konstruksi abstraksi, pengembang kerangka menggunakan pendekatan yang meninggalkan kemungkinan ketika menggunakan abstraksi tingkat tinggi, itu akan turun satu atau beberapa level di bawah ini. Paling sering ini tidak perlu, apalagi, ini membuka kemungkinan senapan ditembak di kaki, tetapi kadang-kadang, dalam kasus yang jarang terjadi, ini mungkin satu-satunya cara untuk memecahkan masalah yang tidak menyelesaikan pada tingkat abstraksi saat ini.

Dengan alat, maksud saya kedua antarmuka program (API) yang disediakan oleh kerangka kerja dan paket pihak ketiga, dan solusi perangkat lunak keseluruhan yang menyederhanakan pencarian untuk masalah yang terkait dengan kode multi-threaded.

Aliran mulai


Kelas Thread, kelas paling dasar dalam .NET untuk bekerja dengan utas. Konstruktor menerima satu dari dua delegasi:

  • ThreadStart - Tanpa Parameter
  • ParametrizedThreadStart - dengan satu parameter tipe objek.

Delegasi akan dieksekusi di utas yang baru dibuat setelah memanggil metode Mulai, jika delegasi tipe ParametrizedThreadStart diteruskan ke konstruktor, maka objek harus diteruskan ke metode Mulai. Mekanisme ini diperlukan untuk mentransfer informasi lokal apa pun ke aliran. Perlu dicatat bahwa membuat utas merupakan operasi yang mahal, dan utas itu sendiri adalah benda berat, setidaknya karena memori 1MB dialokasikan ke tumpukan, dan memerlukan interaksi dengan OS API.

new Thread(...).Start(...); 

Kelas ThreadPool mewakili konsep kumpulan. Di .NET, kumpulan thread adalah karya seni dan pengembang dari Microsoft telah berupaya keras agar dapat berfungsi secara optimal dalam berbagai skenario.

Konsep umum:

Sejak awal, aplikasi di latar belakang membuat beberapa utas sebagai cadangan dan memberikan kesempatan untuk menggunakannya. Jika utas sering digunakan dan dalam jumlah besar, kumpulan dikembangkan untuk memenuhi kebutuhan kode panggilan. Ketika tidak ada arus bebas di kumpulan pada waktu yang tepat, ia akan menunggu salah satu arus untuk kembali atau membuat yang baru. Oleh karena itu, rangkaian utas sangat bagus untuk beberapa tindakan pendek dan kurang cocok untuk operasi yang beroperasi sebagai layanan di seluruh aplikasi.

Untuk menggunakan utas dari kumpulan, ada metode QueueUserWorkItem yang menerima delegasi tipe WaitCallback, yang merupakan tanda tangan yang sama dengan ParametrizedThreadStart, dan parameter yang diteruskan untuk melakukan fungsi yang sama.

 ThreadPool.QueueUserWorkItem(...); 

Metode thread pool yang kurang dikenal, RegisterWaitForSingleObject, digunakan untuk mengatur operasi IO yang tidak menghalangi. Delegasi yang diteruskan ke metode ini akan dipanggil ketika WaitHandle yang diteruskan ke metode ini "Dirilis".

 ThreadPool.RegisterWaitForSingleObject(...) 

.NET memiliki pengatur waktu aliran dan berbeda dari pengatur waktu WinForms / WPF karena penangannya akan dipanggil dalam aliran yang diambil dari kumpulan.

 System.Threading.Timer 

Ada juga cara yang agak eksotis untuk mengirim delegasi ke utas dari kolam - metode BeginInvoke.

 DelegateInstance.BeginInvoke 

Saya juga ingin membahas fungsi yang memanggil banyak metode di atas - CreateThread dari Kernel32.dll Win32 API. Ada cara, berkat mekanisme metode eksternal, untuk memanggil fungsi ini. Saya melihat tantangan semacam itu hanya sekali dalam contoh mengerikan kode warisan, dan motivasi penulis untuk melakukan hal itu masih merupakan misteri bagi saya.

 Kernel32.dll CreateThread 

Lihat dan debug utas


Utas yang Anda buat secara pribadi oleh semua komponen pihak ketiga dan .NET pool dapat dilihat di jendela Utas Visual Studio. Jendela ini akan menampilkan informasi tentang aliran hanya ketika aplikasi sedang dalam debugging dan dalam mode istirahat (mode istirahat). Di sini Anda dapat dengan mudah melihat nama dan prioritas tumpukan setiap utas, beralih debug ke utas tertentu. Properti Priority dari kelas Thread memungkinkan Anda untuk mengatur prioritas utas, yang OC dan CLR akan anggap sebagai rekomendasi ketika membagi waktu CPU antara utas.



Pustaka paralel tugas


Pustaka Paralel Tugas (TPL) muncul di .NET 4.0. Sekarang ini adalah standar dan alat utama untuk bekerja dengan asinkron. Kode apa pun yang menggunakan pendekatan yang lebih lama dianggap sebagai warisan. Unit dasar TPL adalah kelas Tugas dari System.Threading.Tasks namespace. Tugas adalah abstraksi atas utas. Dengan versi baru C #, kami mendapatkan cara yang elegan untuk bekerja dengan operator Task - async / wait. Konsep-konsep ini memungkinkan untuk menulis kode asinkron seolah-olah sederhana dan sinkron, ini memungkinkan bahkan bagi orang-orang dengan sedikit pemahaman tentang dapur utas internal untuk menulis aplikasi yang menggunakannya, aplikasi yang tidak membeku selama operasi yang lama. Menggunakan async / menunggu adalah topik untuk satu atau bahkan beberapa artikel, tetapi saya akan mencoba untuk mendapatkan inti dari beberapa kalimat:

  • async adalah pengubah metode yang mengembalikan Task atau batal
  • dan menunggu adalah pernyataan menunggu tanpa pemblokiran Tugas.

Sekali lagi: operator yang menunggu, dalam kasus umum (ada pengecualian), akan melepaskan utas eksekusi saat ini lebih lanjut, dan ketika Tugas selesai pelaksanaannya, dan utas (sebenarnya lebih tepat untuk mengatakan konteksnya, tetapi lebih pada itu nanti) akan bebas untuk melanjutkan metode lebih lanjut. Di dalam .NET, mekanisme ini diimplementasikan dengan cara yang sama seperti imbal hasil, ketika metode tertulis berubah menjadi seluruh kelas, yang merupakan mesin negara dan dapat dieksekusi dalam potongan terpisah tergantung pada negara-negara ini. Siapa pun yang tertarik dapat menulis kode sederhana apa pun menggunakan asyn / menunggu, kompilasi dan lihat perakitan menggunakan JetBrains dotPeek dengan Compiler Generated Code diaktifkan.

Pertimbangkan opsi untuk meluncurkan dan menggunakan Tugas. Menggunakan contoh kode di bawah ini, kami membuat tugas baru yang tidak berguna ( Thread.Sleep (10000) ), tetapi dalam kehidupan nyata itu harus semacam pekerjaan rumit yang melibatkan CPU.

 using TCO = System.Threading.Tasks.TaskCreationOptions; public static async void VoidAsyncMethod() { var cancellationSource = new CancellationTokenSource(); await Task.Factory.StartNew( // Code of action will be executed on other context () => Thread.Sleep(10000), cancellationSource.Token, TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness, scheduler ); // Code after await will be executed on captured context } 

Tugas dibuat dengan sejumlah opsi:

  • LongRunning adalah isyarat bahwa tugas tidak akan diselesaikan dengan cepat, yang berarti mungkin ada baiknya mempertimbangkan untuk tidak mengambil utas dari kumpulan, tetapi untuk membuat yang terpisah untuk Tugas ini agar tidak membahayakan yang lain.
  • AttachedToParent - Tugas dapat diatur dalam hierarki. Jika opsi ini digunakan, maka Tugas mungkin dalam keadaan ketika telah selesai sendiri dan sedang menunggu anak-anak untuk menyelesaikan.
  • PreferFairness - berarti lebih baik untuk mengeksekusi tugas yang dikirim sebelumnya untuk dieksekusi sebelum yang dikirim nanti. Tapi ini hanya rekomendasi dan hasilnya tidak dijamin.

Parameter kedua untuk metode yang lulus CancellToken. Untuk memproses pembatalan operasi dengan benar setelah peluncurannya, kode yang dieksekusi harus diisi dengan pemeriksaan status dari Cancertoken. Jika tidak ada pemeriksaan, maka metode Batal yang dipanggil pada objek PembatalanTokenSource akan dapat menghentikan pelaksanaan Tugas hanya sebelum dimulai.

Parameter terakhir melewati objek scheduler dari tipe TaskScheduler. Kelas ini dan turunannya dirancang untuk mengontrol strategi untuk mendistribusikan Task'ov oleh utas, secara default, Tugas akan dieksekusi pada utas acak dari kumpulan.

Operator yang menunggu diterapkan ke Tugas yang dibuat, yang berarti kode yang ditulis setelah itu, jika ada, akan dieksekusi dalam konteks yang sama (seringkali ini berarti bahwa ia berada di utas yang sama) seperti kode sebelum menunggu.

Metode ini ditandai sebagai batal async, yang berarti Anda dapat menggunakan operator yang menunggu di dalamnya, tetapi kode panggilan tidak dapat menunggu untuk dieksekusi. Jika fitur ini diperlukan, maka metode tersebut harus mengembalikan Task. Metode yang ditandai batal async cukup umum: sebagai aturan, ini adalah event handler atau metode lain yang bekerja berdasarkan prinsip api dan lupa. Jika Anda tidak hanya perlu memberikan kesempatan untuk menunggu hingga penyelesaian, tetapi juga mengembalikan hasilnya, maka Anda harus menggunakan Tugas.

Pada Tugas yang dikembalikan oleh metode StartNew, seperti yang lain, Anda dapat memanggil metode ConfigureAwait dengan parameter palsu, kemudian eksekusi setelah menunggu tidak akan melanjutkan pada konteks yang ditangkap, tetapi pada yang sewenang-wenang. Ini harus selalu dilakukan ketika konteks eksekusi tidak penting untuk kode setelah menunggu. Ini juga merupakan rekomendasi dari MS ketika menulis kode yang akan dikemas dalam bentuk pustaka.

Mari kita memikirkan sedikit lebih banyak tentang bagaimana Anda bisa menunggu sampai Tugas selesai. Di bawah ini adalah contoh kode, dengan komentar, ketika menunggu dilakukan dengan baik dan saat buruk.

 public static async void AnotherMethod() { int result = await AsyncMethod(); // good result = AsyncMethod().Result; // bad AsyncMethod().Wait(); // bad IEnumerable<Task> tasks = new Task[] { AsyncMethod(), OtherAsyncMethod() }; await Task.WhenAll(tasks); // good await Task.WhenAny(tasks); // good Task.WaitAll(tasks.ToArray()); // bad } 

Pada contoh pertama, kita menunggu Tugas untuk menyelesaikan dan tanpa memblokir utas panggilan, kita akan kembali untuk memproses hasil hanya ketika sudah ada di sana, sampai utas panggilan dibiarkan sendiri.

Pada opsi kedua, kami memblokir utas panggilan sampai hasil metode dihitung. Ini buruk bukan hanya karena kami mengambil utas, sumber daya program yang sangat berharga, dengan kemalasan yang sederhana, tetapi juga karena jika kode metode yang kami panggil telah menunggu, dan konteks sinkronisasi melibatkan kembali ke utas panggilan setelah menunggu, maka kita akan menemui jalan buntu : utas panggilan menunggu sampai hasil dari metode asinkron dihitung, metode asinkron mencoba dengan sia-sia untuk melanjutkan eksekusi di utas panggilan.

Kelemahan lain dari pendekatan ini adalah penanganan kesalahan yang rumit. Faktanya adalah bahwa kesalahan dalam kode asinkron ketika menggunakan async / menunggu sangat mudah untuk ditangani - mereka berperilaku seolah-olah kode itu sinkron. Sementara, jika kita menerapkan pengusiran setan, ekspektasi sinkron ke Tugas, pengecualian asli berubah menjadi AggregateException, yaitu Untuk menangani pengecualian, Anda harus memeriksa tipe InnerException dan menulis rantai if di dalam satu blok tangkap atau menggunakan tangkap ketika membangun alih-alih rantai blok tangkap yang lebih akrab di C #.

Contoh ketiga dan terakhir juga ditandai buruk karena alasan yang sama dan mengandung semua masalah yang sama.

WhenAny dan WhenAll metode sangat mudah dalam menunggu sekelompok Task'ov, mereka membungkus sekelompok Task'ov dalam satu, yang akan bekerja pada operasi pertama Task'a dari grup, atau ketika semua orang menyelesaikan eksekusi mereka.

Aliran berhenti


Karena berbagai alasan, mungkin perlu untuk menghentikan aliran setelah dimulai. Ada beberapa cara untuk melakukan ini. Kelas Thread memiliki dua metode dengan nama yang sesuai - Abort dan Interrupt . Yang pertama tidak direkomendasikan untuk digunakan, karena setelah dipanggil kapan saja secara acak, selama pemrosesan instruksi apa pun, ThreadAbortedException akan dilempar. Anda tidak akan mengharapkan pengecualian seperti itu crash ketika menambahkan variabel integer, kan? Dan ketika menggunakan metode ini, ini adalah situasi yang sangat nyata. Jika Anda ingin mencegah CLR dari melemparkan pengecualian seperti itu di bagian kode tertentu, Anda bisa membungkusnya dengan panggilan ke Thread.BeginCriticalRegion , Thread.EndCriticalRegion . Kode apa pun yang ditulis di blok akhirnya dibungkus dengan panggilan seperti itu. Untuk alasan ini, dalam isi kode kerangka kerja, Anda dapat menemukan blok dengan percobaan kosong, tetapi akhirnya tidak kosong. Microsoft tidak merekomendasikan menggunakan metode ini bahwa mereka tidak memasukkannya dalam .net core.

Metode Interrupt bekerja lebih mudah ditebak. Itu dapat mengganggu utas dengan pengecualian ThreadInterruptedException hanya ketika utas berada dalam status siaga. Dalam keadaan ini, masuk ke penangguhan saat menunggu WaitHandle, mengunci, atau setelah memanggil Thread. Tidur.

Kedua opsi yang dijelaskan di atas buruk karena tidak dapat diprediksi. Solusinya adalah dengan menggunakan struktur CancellingToken dan kelas CancellingTokenSource . Intinya adalah: turunan dari kelas PembatalanTokenSource dibuat dan hanya orang yang memilikinya dapat menghentikan operasi dengan memanggil metode Batal . Hanya PembatalanToken yang diteruskan ke operasi itu sendiri. Pemilik PembatalanToken tidak dapat membatalkan operasi itu sendiri, tetapi hanya dapat memeriksa apakah operasi telah dibatalkan. Untuk melakukan ini, ada properti Boolean IsCancellationRequested dan metode ThrowIfCancelRequested . Yang terakhir akan menaikkan TaskCancelledException jika metode Batal dipanggil pada instance CancellTokenSource yang dibatalkan. Dan inilah metode yang saya rekomendasikan untuk digunakan. Ini lebih baik daripada opsi sebelumnya dengan mendapatkan kontrol penuh pada titik-titik mana operasi pengecualian dapat terputus.

Opsi paling kejam untuk menghentikan utas adalah memanggil fungsi Win32 API TerminateThread. Perilaku CLR setelah memanggil fungsi ini bisa tidak dapat diprediksi. Pada MSDN, berikut ini ditulis tentang fungsi ini: "TerminateThread adalah fungsi berbahaya yang hanya boleh digunakan dalam kasus yang paling ekstrim.

Konversi legacy-API ke Berbasis Tugas menggunakan metode FromAsync


Jika Anda cukup beruntung untuk mengerjakan proyek yang dimulai setelah tugas diperkenalkan dan tidak lagi membuat ngeri bagi sebagian besar pengembang, maka Anda tidak akan harus berurusan dengan banyak API lama, baik pihak ketiga maupun tim Anda disiksa di masa lalu. Untungnya, tim pengembangan .NET Framework merawat kami, meskipun mungkin tujuannya adalah untuk menjaga diri kami sendiri. Meskipun demikian, .NET memiliki sejumlah alat untuk tanpa kesulitan mengubah kode yang ditulis dalam pendekatan pemrograman asinkron lama ke yang baru. Salah satunya adalah metode FromAsync dari TaskFactory. Menggunakan contoh kode di bawah ini, saya membungkus metode asinkron lama dari kelas WebRequest di Tugas menggunakan metode ini.

 object state = null; WebRequest wr = WebRequest.CreateHttp("http://github.com"); await Task.Factory.FromAsync( wr.BeginGetResponse, we.EndGetResponse ); 

Ini hanya sebuah contoh, dan Anda tidak mungkin melakukan ini dengan tipe bawaan, tetapi proyek lama apa pun hanya dipenuhi dengan metode BeginDoSomething yang mengembalikan metode IAsyncResult dan EndDoSomething yang menerimanya.

Konversi legacy-API ke Berbasis Tugas menggunakan kelas TaskCompletionSource


Alat penting lainnya untuk dipertimbangkan adalah kelas TaskCompletionSource . Dalam hal fungsi, tujuan dan prinsip operasi, entah bagaimana dapat mengingatkan metode RegisterWaitForSingleObject dari kelas ThreadPool tentang yang saya tulis di atas. Menggunakan kelas ini, Anda dapat dengan mudah dan nyaman membungkus API asinkron yang lama di Tugas.

Anda akan mengatakan bahwa saya sudah berbicara tentang metode FromAsync dari kelas TaskFactory yang dimaksudkan untuk tujuan ini. Di sini kita harus mengingat seluruh sejarah pengembangan model asinkron di .net yang telah ditawarkan Microsoft selama 15 tahun terakhir: sebelum Pola Asinkron Berbasis Tugas (TAP) ada Pola Pemrograman Asinkron (APP), yang tentang Begin DoMetode metoda yang mengembalikan IAsyncResult dan End Do. Metode yang menerimanya dan metode FromAsync baik-baik saja untuk warisan tahun-tahun ini, tetapi seiring waktu, itu digantikan oleh Event Based Asynchronous Pattern ( EAP ), yang mengasumsikan bahwa suatu peristiwa akan dipicu ketika operasi asinkron selesai.

TaskCompletionSource hanya bagus untuk menyelesaikan tugas dan legacy-API yang dibangun di sekitar model acara. Inti dari karyanya adalah sebagai berikut: objek dari kelas ini memiliki properti publik dari jenis Tugas yang keadaannya dapat dikontrol melalui metode SetResult, SetException, dll dari kelas TaskCompletionSource. Di tempat-tempat di mana operator menunggu diterapkan ke Tugas ini, itu akan dieksekusi atau jatuh dengan pengecualian, tergantung pada metode yang diterapkan pada TaskCompletionSource. Jika semuanya masih tidak jelas, maka mari kita lihat contoh kode ini, di mana beberapa API EAP lama dibungkus dalam Tugas menggunakan TaskCompletionSource: ketika acara dipecat, Tugas akan ditransfer ke status Selesai, dan metode yang diterapkan operator menunggu untuk Tugas ini akan melanjutkan eksekusi mendapatkan objek hasil .

 public static Task<Result> DoAsync(this SomeApiInstance someApiObj) { var completionSource = new TaskCompletionSource<Result>(); someApiObj.Done += result => completionSource.SetResult(result); someApiObj.Do(); result completionSource.Task; } 

Tips & Trik TaskCompletionSource


Membungkus API yang lebih lama tidak semuanya dapat Anda lakukan dengan TaskCompletionSource. Menggunakan kelas ini membuka kemungkinan menarik untuk merancang berbagai API pada tugas yang tidak menempati utas. Dan aliran, seperti yang kita ingat, adalah sumber daya yang mahal dan jumlahnya terbatas (terutama oleh RAM). Batasan ini mudah dicapai dengan mengembangkan, misalnya, aplikasi web yang dimuat dengan logika bisnis yang kompleks. Pertimbangkan kemungkinan yang saya bicarakan tentang penerapan trik seperti Polling Panjang.

Secara singkat, inti dari triknya adalah ini: Anda perlu menerima informasi dari API tentang beberapa peristiwa yang terjadi di sisinya, sementara API karena alasan tertentu tidak dapat melaporkan acara tersebut, tetapi hanya dapat mengembalikan negara. Contohnya adalah semua API yang dibangun di atas HTTP sebelum waktu WebSocket atau ketika tidak mungkin karena alasan tertentu untuk menggunakan teknologi ini. Klien dapat menanyakan server HTTP. Server HTTP tidak dapat dengan sendirinya memprovokasi komunikasi dengan klien. Solusi sederhana adalah menginterogasi server dengan timer, tetapi ini menciptakan beban tambahan pada server dan penundaan tambahan rata-rata TimerInterval / 2. Untuk menghindari hal ini, sebuah trik yang disebut Long Polling ditemukan, yang melibatkan menunda respons dari server sampai Timeout berakhir atau suatu peristiwa akan terjadi. Jika suatu peristiwa telah terjadi, maka itu diproses, jika tidak, permintaan dikirim lagi.

 while(!eventOccures && !timeoutExceeded) { CheckTimout(); CheckEvent(); Thread.Sleep(1); } 

Tetapi solusi seperti itu akan muncul dengan sendirinya begitu jumlah klien yang menunggu acara bertambah, karena Setiap klien seperti itu, untuk mengantisipasi acara, mengambil seluruh aliran. Ya, dan kami mendapat penundaan tambahan 1 ms untuk pengoperasian acara, paling sering itu tidak signifikan, tetapi mengapa membuat perangkat lunak lebih buruk daripada yang seharusnya? Jika Anda menghapus Thread.Sleep (1), maka sia-sia kami akan memuat satu inti prosesor pada idle 100%, berputar dalam siklus yang tidak berguna. Menggunakan TaskCompletionSource, Anda dapat dengan mudah mengulang kode ini dan menyelesaikan semua masalah yang diidentifikasi di atas:

 class LongPollingApi { private Dictionary<int, TaskCompletionSource<Msg>> tasks; public async Task<Msg> AcceptMessageAsync(int userId, int duration) { var cs = new TaskCompletionSource<Msg>(); tasks[userId] = cs; await Task.WhenAny(Task.Delay(duration), cs.Task); return cs.Task.IsCompleted ? cs.Task.Result : null; } public void SendMessage(int userId, Msg m) { if (tasks.TryGetValue(userId, out var completionSource)) completionSource.SetResult(m); } } 

Kode ini tidak siap produksi, tetapi hanya demo. Untuk menggunakannya dalam kasus nyata, Anda juga harus setidaknya menangani situasi ketika pesan tiba pada saat tidak ada yang mengharapkannya: dalam kasus ini, metode AsseptMessageAsync harus mengembalikan Tugas yang sudah selesai. Jika kasus ini adalah yang paling sering terjadi, maka Anda dapat berpikir tentang menggunakan ValueTask.

Ketika kami menerima permintaan untuk pesan, kami membuat dan menempatkan TaskCompletionSource dalam kamus, dan kemudian kami menunggu apa yang terjadi terlebih dahulu: interval waktu yang ditentukan berakhir atau pesan diterima.

ValueTask: mengapa dan bagaimana


Async / menunggu operator, seperti operator pengembalian hasil, menghasilkan mesin negara dari metode, yang menciptakan objek baru, yang hampir selalu tidak penting, tetapi dalam kasus yang jarang terjadi dapat membuat masalah. Kasing ini mungkin metode yang disebut sangat sering, berbicara tentang puluhan dan ratusan ribu panggilan per detik. Jika metode seperti itu ditulis sehingga dalam banyak kasus mengembalikan hasil melewati semua metode menunggu, maka .NET menyediakan alat untuk mengoptimalkan ini - struktur ValueTask. Untuk membuatnya jelas, pertimbangkan contoh penggunaannya: ada cache yang sering kita kunjungi. Ada beberapa nilai di dalamnya dan kemudian kita mengembalikannya, jika tidak, maka kita pergi ke IO lambat di belakangnya. Saya ingin melakukan yang terakhir secara asinkron, yang berarti seluruh metode asinkron. Jadi, cara yang jelas untuk menulis sebuah metode adalah sebagai berikut:

 public async Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return val; return await RequestById(id); } 

Karena keinginan untuk mengoptimalkan sedikit, dan sedikit ketakutan tentang apa yang akan dihasilkan Roslyn dengan mengkompilasi kode ini, kita dapat menulis ulang contoh ini sebagai berikut:

 public Task<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return Task.FromResult(val); return RequestById(id); } 

Memang, solusi optimal dalam hal ini adalah untuk mengoptimalkan hot-path, yaitu, mendapatkan nilai dari kamus tanpa alokasi tambahan dan memuat pada GC, sedangkan dalam kasus-kasus langka ketika kita masih perlu pergi ke IO, semuanya akan tetap ditambah / minus yang lama:

 public ValueTask<string> GetById(int id) { if (cache.TryGetValue(id, out string val)) return new ValueTask<string>(val); return new ValueTask<string>(RequestById(id)); } 

Mari kita lihat lebih dekat fragmen kode ini: jika ada nilai dalam cache, kita membuat struktur, jika tidak, tugas sebenarnya akan dibungkus dengan signifikan. Kode panggilan tidak peduli ke arah mana kode ini dijalankan: ValueTask, dalam hal sintaksis C #, akan berperilaku seperti Tugas biasa dalam kasus ini.

Penjadwal Tugas: Mengelola Strategi Peluncuran Tugas


API berikutnya yang ingin saya pertimbangkan adalah kelas TaskScheduler dan turunannya. Saya sudah menyebutkan di atas bahwa di TPL ada kemampuan untuk mengendalikan strategi untuk mendistribusikan Task'ov dengan utas. Strategi tersebut didefinisikan dalam turunan dari kelas TaskScheduler. Hampir semua strategi yang Anda butuhkan akan ditemukan di perpustakaan ParallelExtensionsExtras , yang dikembangkan oleh microsoft, tetapi bukan bagian dari .NET, tetapi dikirimkan sebagai paket Nuget. Mari kita pertimbangkan secara singkat beberapa di antaranya:

  • CurrentThreadTaskScheduler - Melakukan Tugas pada utas saat ini
  • LimitedConcurrencyLevelTaskScheduler - membatasi jumlah tugas yang dieksekusi secara bersamaan ke parameter N, yang diterima di konstruktor
  • OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1), .
  • WorkStealingTaskSchedulerwork-stealing . ThreadPool. , .NET ThreadPool , , . . .. WorkStealingTaskScheduler' , ThreadPool .
  • QueuedTaskScheduler - memungkinkan Anda untuk melakukan tugas sesuai dengan aturan antrian dengan prioritas
  • ThreadPerTaskScheduler - membuat utas terpisah untuk setiap Tugas yang berjalan di atasnya. Ini bisa berguna untuk tugas-tugas yang berjalan lama tak terduga.

Ada artikel terperinci yang bagus tentang Penjadwal Tugas di blog microsoft.

Untuk debugging nyaman segala sesuatu yang berkaitan dengan Tugas di Visual Studio ada jendela Tugas. Di jendela ini, Anda dapat melihat status tugas saat ini dan pergi ke baris kode yang sedang dijalankan.



PLinq dan kelas Paralel


Selain Tugas dan semua yang dikatakan bersama mereka di .NET, ada dua alat yang lebih menarik: PLinq (Linq2 Paralel) dan kelas Paralel. Yang pertama menjanjikan eksekusi paralel dari semua operasi Linq pada banyak utas. Jumlah utas dapat dikonfigurasi dengan metode ekstensi WithDegreeOfParallelism. Sayangnya, paling sering PLinq dalam mode run secara default tidak akan memiliki cukup informasi tentang bagian dalam sumber data Anda untuk memberikan peningkatan kecepatan yang signifikan, di sisi lain, harga percobaan sangat rendah: Anda hanya perlu memanggil metode AsParallel di depan rantai metode Linq dan melakukan tes kinerja. Selain itu, dimungkinkan untuk mentransfer ke informasi tambahan PLinq tentang sifat sumber data Anda menggunakan mekanisme Partisi. Anda dapat membaca lebih lanjut di sini dan di sini..

Kelas statis paralel menyediakan metode untuk mengulangi koleksi Foreach secara paralel, mengeksekusi loop For, dan mengeksekusi beberapa delegasi secara paralel ke Invoke. Eksekusi utas saat ini akan dihentikan hingga akhir perhitungan. Jumlah utas dapat dikonfigurasi dengan meneruskan ParallelOptions sebagai argumen terakhir. Dengan menggunakan opsi, Anda juga dapat menentukan TaskScheduler dan CancellingToken.

Kesimpulan


Ketika saya mulai menulis artikel ini berdasarkan bahan-bahan laporan saya dan informasi yang saya kumpulkan selama pekerjaan setelahnya, saya tidak berharap akan menghasilkan begitu banyak. Sekarang, ketika editor teks di mana saya mengetik artikel ini dengan mencela mengatakan bahwa halaman ke-15 telah hilang, saya akan meringkas hasil antara. Trik, API, alat visual, dan jebakan lain akan dibahas dalam artikel mendatang.

Kesimpulan:

  • Anda perlu mengetahui alat untuk bekerja dengan utas, asinkron, dan paralelisme untuk menggunakan sumber daya PC modern.
  • .NET memiliki banyak alat berbeda untuk tujuan ini.
  • Tidak semuanya muncul sekaligus, karena peninggalan sering ditemukan, namun ada cara untuk mengonversi API lama tanpa banyak usaha.
  • Bekerja dengan utas di .NET diwakili oleh kelas utas dan Utas
  • Thread.Abort, Thread.Interrupt, Win32 API TerminateThread . CancellationToken'
  • — , . , . TaskCompletionSource
  • .NET Task'.
  • c# async/await
  • Task' TaskScheduler'
  • ValueTask hot-paths memory-traffic
  • Tasks Threads Visual Studio
  • PLinq , , partitioning
  • ...

Source: https://habr.com/ru/post/id452094/


All Articles