.NET - Alat untuk bekerja dengan multithreading dan asynchrony - Bagian 2

Saya awalnya memposting artikel ini di blog CodingSight .
Ini juga tersedia dalam bahasa Rusia di sini .

Artikel ini berisi bagian kedua dari pidato saya di pertemuan multithreading. Anda dapat melihat bagian pertama di sini dan di sini . Pada bagian pertama, saya fokus pada seperangkat alat dasar yang digunakan untuk memulai utas atau Tugas, cara untuk melacak keadaan mereka, dan beberapa hal rapi lainnya seperti PLinq. Pada bagian ini, saya akan memperbaiki masalah yang mungkin Anda temui di lingkungan multi-threaded dan beberapa cara untuk mengatasinya.

Isi




Mengenai sumber daya bersama


Anda tidak dapat menulis program yang pekerjaannya didasarkan pada banyak utas tanpa memiliki sumber daya bersama. Bahkan jika itu berfungsi pada level abstraksi Anda saat ini, Anda akan menemukan bahwa itu benar-benar telah membagikan sumber daya begitu Anda turun satu atau beberapa level abstraksi. Berikut ini beberapa contohnya:

Contoh # 1:

Untuk menghindari kemungkinan masalah, Anda membuat utas berfungsi dengan file berbeda, satu file untuk setiap utas. Tampaknya bagi Anda bahwa program tersebut tidak memiliki sumber daya bersama apa pun.

Turun beberapa tingkat ke bawah, Anda akan tahu bahwa hanya ada satu hard drive, dan terserah pada driver atau OS untuk menemukan solusi untuk masalah dengan akses hard drive.

Contoh # 2:

Setelah membaca contoh # 1 , Anda memutuskan untuk meletakkan file pada dua mesin jarak jauh yang berbeda dengan perangkat keras dan sistem operasi yang berbeda secara fisik. Anda juga memelihara dua koneksi FTP atau NFS yang berbeda.

Turun beberapa tingkat lagi, Anda memahami bahwa tidak ada yang benar-benar berubah, dan masalah akses kompetitif sekarang didelegasikan ke driver kartu jaringan atau OS mesin di mana program ini berjalan.

Contoh # 3:

Setelah mencabut sebagian besar rambut Anda di atas upaya membuktikan Anda dapat menulis program multi-berulir, Anda memutuskan untuk membuang file sepenuhnya dan memindahkan perhitungan ke dua objek yang berbeda, dengan tautan ke masing-masing objek yang tersedia hanya untuk spesifik mereka utas.

Untuk memalu selusin paku terakhir ke peti mati ide ini: satu runtime dan Garbage Collector, satu penjadwal ulir, secara fisik satu RAM terpadu, dan satu prosesor masih dianggap sebagai sumber daya bersama.

Jadi, kami belajar bahwa tidak mungkin untuk menulis program multi-threaded tanpa sumber daya bersama pada semua tingkat abstraksi dan pada seluruh ruang lingkup tumpukan teknologi. Untungnya, setiap tingkat abstraksi (sebagai aturan umum) sebagian atau bahkan sepenuhnya menangani masalah akses kompetitif atau langsung menolaknya (misalnya: kerangka kerja UI tidak memungkinkan bekerja dengan elemen dari utas berbeda). Jadi biasanya, masalah dengan sumber daya bersama muncul di tingkat abstraksi Anda saat ini. Untuk mengatasinya, konsep sinkronisasi diperkenalkan.

Kemungkinan masalah di lingkungan multi-utas


Kami dapat mengklasifikasikan kesalahan perangkat lunak ke dalam kategori berikut:
  1. Program tidak membuahkan hasil - macet atau macet.
  2. Program memberikan hasil yang salah.
  3. Program ini menghasilkan hasil yang benar tetapi tidak memenuhi beberapa persyaratan yang tidak terkait fungsi - ia menghabiskan terlalu banyak waktu atau sumber daya.

Dalam lingkungan multi-utas, masalah utama yang menghasilkan kesalahan # 1 dan # 2 adalah kondisi jalan buntu dan ras .


Jalan buntu


Kebuntuan adalah blok timbal balik. Ada banyak variasi kebuntuan. Yang berikut ini dapat dianggap sebagai yang paling umum:



Sementara Thread # 1 sedang melakukan sesuatu, Thread # 2 memblokir sumber B. Beberapa waktu kemudian, Thread # 1 memblokir sumber daya A dan mencoba untuk memblokir sumber daya B. Sayangnya, ini tidak akan pernah terjadi karena Thread # 2 hanya akan melepaskan sumber daya B setelah memblokir sumber daya A.

Kondisi balapan


Race-Condition adalah situasi ketika keduanya, perilaku dan hasil perhitungan tergantung pada penjadwal thread dari lingkungan eksekusi

Masalahnya adalah bahwa program Anda dapat bekerja secara tidak benar satu kali dalam seratus, atau bahkan dalam jutaan.

Hal-hal mungkin menjadi lebih buruk ketika masalah datang bertiga. Sebagai contoh, perilaku spesifik dari penjadwal thread dapat menyebabkan kebuntuan timbal balik.

Selain dua masalah ini yang menyebabkan kesalahan eksplisit, ada juga masalah yang, jika tidak mengarah ke hasil perhitungan yang salah, masih dapat membuat program mengambil lebih banyak waktu atau sumber daya untuk menghasilkan hasil yang diinginkan. Dua dari masalah tersebut adalah Busy Wait dan Thread Starvation .

Sibuk-tunggu


Busy Wait adalah masalah yang terjadi ketika program menghabiskan sumber daya prosesor menunggu daripada pada perhitungan.

Biasanya, masalah ini terlihat seperti berikut:

while(!hasSomethingHappened) ; 

Ini adalah contoh kode yang sangat buruk karena sepenuhnya menempati satu inti dari prosesor Anda tanpa benar-benar melakukan sesuatu yang produktif sama sekali. Kode tersebut hanya dapat dibenarkan ketika sangat penting untuk dengan cepat memproses perubahan nilai di utas yang berbeda. Dan dengan 'cepat' maksud saya Anda tidak bisa menunggu bahkan untuk beberapa nanodetik. Dalam semua kasus lain, yaitu, semua kasus dapat muncul pikiran yang masuk akal, jauh lebih nyaman untuk menggunakan variasi ResetEvent dan versi Slim mereka. Kami akan membicarakannya sedikit nanti.

Mungkin, beberapa pembaca akan menyarankan menyelesaikan masalah satu inti sepenuhnya sibuk dengan menunggu dengan menambahkan Thread. Tidur (1) (atau yang serupa) ke dalam siklus. Sementara itu akan menyelesaikan masalah ini, yang baru akan dibuat - waktu yang dibutuhkan untuk bereaksi terhadap perubahan sekarang akan rata-rata 0,5 ms. Di satu sisi, ini tidak banyak, tetapi di sisi lain, nilai ini lebih tinggi dari apa yang dapat kita capai dengan menggunakan primitif sinkronisasi dari keluarga ResetEvent.

Kelaparan benang


Thread Starvation adalah masalah dengan program yang memiliki terlalu banyak thread yang beroperasi secara bersamaan. Di sini, kita berbicara secara khusus tentang utas yang sibuk dengan perhitungan daripada menunggu jawaban dari beberapa IO. Dengan masalah ini, kami kehilangan semua manfaat kinerja yang mungkin muncul seiring dengan utas karena prosesor menghabiskan banyak waktu untuk beralih konteks.

Anda dapat menemukan masalah seperti itu dengan menggunakan berbagai profiler. Berikut ini adalah tangkapan layar dari profiler dotTrace yang bekerja dalam mode Timeline

(klik untuk memperbesar).

Biasanya, program yang tidak menderita kelaparan utas tidak memiliki bagian berwarna merah muda pada bagan yang mewakili utas. Selain itu, dalam kategori Subsistem, kita dapat melihat bahwa program sedang menunggu CPU untuk 30,6% dari waktu.

Ketika masalah seperti itu didiagnosis, Anda bisa mengatasinya secara sederhana: Anda telah memulai terlalu banyak utas sekaligus, jadi mulailah lebih sedikit utas.

Metode sinkronisasi



Saling bertautan


Ini mungkin metode sinkronisasi yang paling ringan. Saling bertautan adalah serangkaian operasi atom sederhana. Ketika operasi atom sedang dijalankan, tidak ada yang bisa terjadi. Dalam .NET, Interlocked diwakili oleh kelas statis dengan nama yang sama dengan pilihan metode, masing-masing menerapkan satu operasi atom.

Untuk mewujudkan kengerian utama operasi non-atom, coba tulis sebuah program yang meluncurkan 10 utas, masing-masing meningkatkan variabel yang sama satu juta kali lipat. Ketika mereka selesai dengan pekerjaan mereka, output nilai dari variabel ini. Sayangnya, ini akan sangat berbeda dari 10 juta. Selain itu, akan berbeda setiap kali Anda menjalankan program. Ini terjadi karena bahkan operasi sederhana seperti kenaikan itu bukan operasi atom, dan termasuk ekstraksi nilai dari memori, perhitungan nilai baru dan menulisnya ke memori lagi. Jadi, dua utas dapat membuat salah satu dari operasi ini dan peningkatan akan hilang dalam kasus ini.

Kelas yang saling bertautan menyediakan metode Peningkatan / Penurunan, dan tidak sulit untuk menebak apa yang seharusnya mereka lakukan. Mereka sangat berguna jika Anda memproses data dalam beberapa utas dan menghitung sesuatu. Kode seperti itu akan bekerja lebih cepat daripada kunci klasik. Jika kami menggunakan Saling Bertautan dalam situasi yang dijelaskan dalam paragraf sebelumnya, program akan menghasilkan nilai 10 juta dalam skenario apa pun.

Fungsi metode CompareExchange tidak begitu jelas. Namun, keberadaannya memungkinkan implementasi banyak algoritma yang menarik. Yang paling penting, yang dari keluarga bebas kunci.

 public static int CompareExchange (ref int location1, int value, int comparand); 

Metode ini mengambil tiga nilai. Yang pertama dilewatkan melalui referensi dan itu adalah nilai yang akan diubah ke yang kedua jika location1 sama dengan perbandingan dan ketika perbandingan dilakukan. Nilai asli location1 akan dikembalikan. Ini terdengar rumit, jadi lebih mudah untuk menulis sepotong kode yang melakukan operasi yang sama dengan CompareExchange:

 var original = location1; if (location1 == comparand) location1 = value; return original; 

Satu-satunya perbedaan adalah bahwa kelas yang saling bertautan mengimplementasikan ini dalam cara atom. Jadi, jika kita menulis kode ini sendiri, kita bisa menghadapi skenario di mana kondisi location1 == comparand telah terpenuhi. Tetapi ketika pernyataan location1 = nilai dieksekusi, utas yang berbeda telah mengubah nilai location1, sehingga akan hilang.

Kita dapat menemukan contoh yang baik tentang bagaimana metode ini dapat digunakan dalam kode yang dihasilkan oleh kompiler untuk setiap peristiwa C #.

Mari kita menulis kelas sederhana dengan satu acara bernama MyEvent:

 class MyClass { public event EventHandler MyEvent; } 

Sekarang, mari kita bangun proyek dalam konfigurasi Release dan buka build through dotPeek dengan opsi "Show Compiler Generated Code" diaktifkan:

 [CompilerGenerated] private EventHandler MyEvent; public event EventHandler MyEvent { [CompilerGenerated] add { EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange<EventHandler>(ref this.MyEvent, (EventHandler) Delegate.Combine((Delegate) comparand, (Delegate) value), comparand); } while (eventHandler != comparand); } [CompilerGenerated] remove { // The same algorithm but with Delegate.Remove } } 

Di sini, kita dapat melihat bahwa kompiler telah menghasilkan algoritma yang agak rumit di belakang layar. Algoritma ini mencegah kita dari kehilangan berlangganan ke acara di mana beberapa utas secara bersamaan berlangganan ke acara ini. Mari kita uraikan metode add sambil mengingat apa yang dilakukan metode CompareExchange di balik layar:

 EventHandler eventHandler = this.MyEvent; EventHandler comparand; do { comparand = eventHandler; // Begin Atomic Operation if (MyEvent == comparand) { eventHandler = MyEvent; MyEvent = Delegate.Combine(MyEvent, value); } // End Atomic Operation } while (eventHandler != comparand); 

Ini jauh lebih mudah dikelola, tetapi mungkin masih membutuhkan penjelasan. Ini adalah bagaimana saya menggambarkan algoritma:

Jika MyEvent masih sama seperti pada saat kami mulai menjalankan Delegate.Combine, kemudian atur ke Delegate.Combine kembali. Jika tidak, coba lagi sampai berfungsi.

Dengan cara ini, langganan tidak akan pernah hilang. Anda harus menyelesaikan masalah serupa jika Anda ingin mengimplementasikan array dinamis, aman-thread, dan bebas-kunci. Jika beberapa utas tiba-tiba mulai menambahkan elemen ke array itu, penting agar semua elemen tersebut berhasil ditambahkan.

Monitor. Masukkan, Monitor. Keluar, kunci


Konstruksi ini paling sering digunakan untuk sinkronisasi ulir. Mereka menerapkan konsep bagian kritis: yaitu, kode yang ditulis antara panggilan Monitor. Masukkan dan Monitor. Keluar hanya dapat dieksekusi pada satu sumber daya pada satu titik waktu dengan hanya satu utas. Operator kunci berfungsi sebagai sintaks-gula di sekitar panggilan Enter / Exit yang dibungkus try-akhirnya. Kualitas yang menyenangkan dari bagian kritis dalam .NET adalah mendukung reentrancy. Ini berarti bahwa kode berikut dapat dieksekusi tanpa masalah nyata:

 lock(a) { lock (a) { ... } } 

Tidak mungkin ada orang yang menulis dengan cara yang persis seperti ini, tetapi jika Anda menyebarkan kode ini di antara beberapa metode melalui kedalaman tumpukan-panggilan, fitur ini dapat menghemat beberapa IFs. Agar trik ini berfungsi, pengembang .NET harus menambahkan batasan - Anda hanya dapat menggunakan contoh tipe referensi sebagai objek sinkronisasi, dan beberapa byte ditambahkan ke setiap objek tempat pengenal utas akan ditulis.

Keunikan proses kerja bagian kritis ini dalam C # memberlakukan satu batasan menarik pada operator kunci: Anda tidak dapat menggunakan operator menunggu di dalam operator kunci. Pada awalnya, ini mengejutkan saya karena Monitor yang coba-akhirnya serupa. Konstruksi Masuk / Keluar dapat dikompilasi. Apa masalahnya? Penting untuk membaca kembali paragraf sebelumnya dan menerapkan pengetahuan tentang bagaimana async / menunggu berfungsi: kode setelah menunggu tidak akan dieksekusi pada utas yang sama dengan kode sebelum menunggu. Ini tergantung pada konteks sinkronisasi dan apakah metode ConfigureAwait dipanggil atau tidak. Dari sini, berikut Monitor.Exit dapat dieksekusi pada utas yang berbeda dari Monitor.Enter, yang akan menyebabkan SynchronizationLockException dilemparkan. Jika Anda tidak percaya, coba jalankan kode berikut di aplikasi konsol - itu akan menghasilkan SynchronizationLockException :

 var syncObject = new Object(); Monitor.Enter(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Monitor.Exit(syncObject); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); 

Perlu dicatat bahwa, dalam aplikasi WinForms atau WPF, kode ini akan berfungsi dengan benar jika Anda memanggilnya dari utas utama karena akan ada konteks sinkronisasi yang mengimplementasikan kembali ke UI-Utas setelah panggilan menunggu. Bagaimanapun, lebih baik tidak bermain-main dengan bagian-bagian penting dalam konteks kode yang berisi operator yang menunggu. Dalam contoh seperti itu, lebih baik menggunakan primitif sinkronisasi yang akan kita bahas nanti.

Sementara kita berada pada topik bagian kritis dalam. NET, penting untuk menyebutkan satu lagi kekhasan bagaimana mereka diterapkan. Bagian kritis dalam .NET berfungsi dalam dua mode: spin-wait dan core-wait. Kami dapat mewakili algoritma spin-tunggu seperti kodesemu berikut:

 while(!TryEnter(syncObject)) ; 

Optimalisasi ini diarahkan untuk menangkap bagian kritis secepat mungkin dalam waktu singkat dengan dasar bahwa, bahkan jika sumber daya saat ini ditempati, itu akan dirilis segera. Jika ini tidak terjadi dalam waktu singkat, utas akan beralih ke menunggu dalam mode inti, yang membutuhkan waktu - sama seperti mundur dari menunggu. Para pengembang .NET telah mengoptimalkan skenario blok pendek sebanyak mungkin. Sayangnya, jika banyak utas mulai menarik bagian kritis di antara mereka, itu dapat menyebabkan beban CPU yang tiba-tiba tinggi.

SpinLock, SpinWait


Setelah menyebutkan algoritma menunggu siklik (spin-tunggu), ada baiknya berbicara tentang struktur SpinLock dan SpinWait dari BCL. Anda harus menggunakannya jika ada alasan untuk menganggapnya akan selalu mungkin untuk mendapatkan blok dengan sangat cepat. Di sisi lain, Anda seharusnya tidak benar-benar memikirkannya sampai hasil profiling akan menunjukkan bahwa kemacetan program Anda disebabkan oleh penggunaan primitif sinkronisasi lainnya.

Monitor. Tunggu, Monitor. Tolong [Semua]


Kita harus melihat kedua metode ini secara berdampingan. Dengan bantuan mereka, Anda dapat menerapkan berbagai skenario Produsen-Konsumen.

Produser-Konsumen adalah pola desain multi-proses / multi-utas yang menyiratkan satu atau lebih utas / proses yang menghasilkan data dan satu atau lebih proses / utas yang memproses data ini. Biasanya, koleksi bersama digunakan.

Kedua metode ini hanya dapat dipanggil oleh utas yang saat ini memiliki blok. Metode Tunggu akan melepaskan blok dan membeku sampai utas lainnya akan memanggil Pulse.

Sebagai contohnya, saya menulis sedikit contoh:

 object syncObject = new object(); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start(); 

(Saya menggunakan gambar daripada teks di sini untuk secara akurat menunjukkan perintah pelaksanaan instruksi)
Penjelasan: Saya menetapkan latensi 100 ms saat memulai utas kedua untuk secara khusus menjamin bahwa itu akan dieksekusi nanti.
- T1: Baris # 2 utas dimulai
- T1: Baris # 3 utas memasuki bagian kritis
- T1: Baris # 6 utas tertidur
- T2: Baris # 3 utas dimulai
- T2: Baris # 4 membeku dan menunggu bagian kritis
- T1: Baris # 7 itu membiarkan bagian kritis pergi dan membeku sambil menunggu Pulsa keluar
- T2: Baris # 8 memasuki bagian kritis
- T2: Baris # 11 memberi sinyal T1 dengan bantuan Pulse
- T2: Baris # 14 keluar dari bagian kritis. T1 tidak dapat melanjutkan eksekusi sebelum ini terjadi.
- T1: Baris # 15 keluar dari menunggu
- T1: Baris # 16 keluar dari bagian kritis

Ada komentar penting dalam MSDN mengenai penggunaan metode Pulse / Tunggu: Monitor tidak menyimpan informasi keadaan, yang berarti memanggil metode Pulse sebelum metode Tunggu dapat menyebabkan kebuntuan. Jika hal ini memungkinkan, lebih baik menggunakan salah satu kelas dari keluarga ResetEvent.

Contoh sebelumnya dengan jelas menunjukkan bagaimana metode Tunggu / Denyut dari kelas Monitor berfungsi, tetapi masih menyisakan beberapa pertanyaan tentang kasus-kasus di mana kita harus menggunakannya. Contoh yang baik adalah implementasi BlockingQueue <T> ini. Di sisi lain, implementasi BlockingCollection <T> dari System.Collections.Concurrent menggunakan SemaphoreSlim untuk sinkronisasi.

ReaderWriterLockSlim


Saya sangat suka primitif sinkronisasi ini, dan itu diwakili oleh kelas dengan nama yang sama dari System.Threading namespace. Saya pikir banyak program akan bekerja lebih baik jika pengembang mereka menggunakan kelas ini daripada kunci standar.

Ide: banyak utas dapat membaca, dan satu-satunya yang bisa menulis. Ketika utas ingin menulis, bacaan baru tidak dapat dimulai - mereka akan menunggu penulisan sampai akhir. Ada juga konsep upgrade-baca-kunci. Anda dapat menggunakannya ketika, selama proses membaca, Anda memahami ada kebutuhan untuk menulis sesuatu - kunci tersebut akan diubah menjadi kunci-tulis dalam satu operasi atom.

Di System.Threading namespace, ada juga kelas ReadWriteLock, tetapi sangat disarankan untuk tidak menggunakannya untuk pengembangan baru. Versi Slim akan membantu menghindari kasus-kasus yang menyebabkan kebuntuan dan memungkinkan untuk dengan cepat menangkap blok karena mendukung sinkronisasi dalam mode spin-wait sebelum masuk ke mode inti.

Jika Anda tidak tahu tentang kelas ini sebelum membaca artikel ini, saya pikir sekarang, Anda telah mengingat banyak contoh dari kode yang baru ditulis di mana pendekatan ini untuk blok memungkinkan program untuk bekerja secara efektif.

Antarmuka kelas ReaderWriterLockSlim sederhana dan mudah dipahami, tetapi tidak nyaman untuk digunakan:

 var @lock = new ReaderWriterLockSlim(); @lock.EnterReadLock(); try { // ... } finally { @lock.ExitReadLock(); } 

Saya biasanya suka membungkusnya dalam kelas - ini membuatnya lebih mudah.

Ide: buat metode Baca / WriteLock yang mengembalikan objek beserta metode Buang. Anda kemudian dapat mengaksesnya di Menggunakan, dan itu mungkin tidak akan jauh berbeda dari kunci standar dalam hal jumlah baris.

 class RWLock : IDisposable { public struct WriteLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public WriteLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterWriteLock(); } public void Dispose() => @lock.ExitWriteLock(); } public struct ReadLockToken : IDisposable { private readonly ReaderWriterLockSlim @lock; public ReadLockToken(ReaderWriterLockSlim @lock) { this.@lock = @lock; @lock.EnterReadLock(); } public void Dispose() => @lock.ExitReadLock(); } private readonly ReaderWriterLockSlim @lock = new ReaderWriterLockSlim(); public ReadLockToken ReadLock() => new ReadLockToken(@lock); public WriteLockToken WriteLock() => new WriteLockToken(@lock); public void Dispose() => @lock.Dispose(); } 

Ini memungkinkan kami untuk hanya menulis yang berikut di kode nanti:

 var rwLock = new RWLock(); // ... using(rwLock.ReadLock()) { // ... } 


Keluarga ResetEvent


Saya menyertakan kelas-kelas berikut dalam keluarga ini: ManualResetEvent, ManualResetEventSlim, dan AutoResetEvent.

Kelas ManualResetEvent, versi Slim-nya, dan kelas AutoResetEvent dapat ada di dua negara:

- Non-sinyal - dalam keadaan ini, semua utas yang telah memanggil WaitOne dibekukan hingga acara beralih ke status bersinyal.
- Ditandatangani - dalam kondisi ini, semua utas yang sebelumnya dibekukan pada panggilan WaitOne dilepaskan. Semua panggilan WaitOne baru pada acara yang ditandai dilakukan secara relatif instan.

AutoResetEvent berbeda dari ManualResetEvent dalam hal itu secara otomatis beralih ke keadaan tanpa sinyal setelah melepaskan tepat satu utas . Jika beberapa utas dibekukan saat menunggu AutoResetEvent, maka memanggil Set hanya akan merilis satu utas acak, sebagai lawan dari ManualResetEvent yang melepaskan semua utas.

Mari kita lihat contoh bagaimana AutoResetEvent bekerja:

 AutoResetEvent evt = new AutoResetEvent(false); Thread t1 = new Thread(T1); t1.Start(); Thread.Sleep(100); Thread t2 = new Thread(T2); t2.Start(); 

Dalam contoh-contoh ini, kita dapat melihat bahwa peristiwa tersebut beralih ke keadaan tanpa sinyal secara otomatis hanya setelah melepaskan utas yang dibekukan pada panggilan WaitOne.

Tidak seperti ReaderWriterLock, ManualResetEvent tidak dianggap usang bahkan setelah versi Slim-nya muncul. Versi Slim kelas ini bisa efektif untuk waktu yang singkat karena terjadi dalam mode Spin-Wait; versi standarnya bagus untuk menunggu lama.

Terlepas dari kelas ManualResetEvent dan AutoResetEvent, ada juga kelas CountdownEvent. Kelas ini sangat berguna untuk mengimplementasikan algoritma yang menggabungkan hasil bersama setelah bagian paralel. Pendekatan ini dikenal sebagai fork-join . Ada artikel bagus yang didedikasikan untuk kelas ini, jadi saya tidak akan menjelaskannya secara rinci di sini.

Kesimpulan


  • Saat bekerja dengan utas, ada dua masalah yang dapat menyebabkan hasil yang salah atau bahkan tidak ada hasil - kondisi balapan dan kebuntuan.
  • Masalah yang dapat membuat program menghabiskan lebih banyak waktu atau sumber daya adalah thread kelaparan dan menunggu sibuk.
  • .NET menyediakan banyak cara untuk menyinkronkan utas.
  • Ada dua mode tunggu blok - Putar Tunggu dan Tunggu Inti. Kadang-kadang primitif sinkronisasi utas di .NET menggunakan keduanya.
  • Saling bertautan adalah seperangkat operasi atom yang dapat digunakan untuk mengimplementasikan algoritma bebas kunci. Ini primitif sinkronisasi tercepat.
  • Penguncian dan Monitor. Operator Enter / Exit mengimplementasikan konsep bagian kritis - sebuah fragmen kode yang hanya dapat dieksekusi oleh satu utas pada satu titik waktu.
  • Metode Monitor.Pulse / Tunggu berguna untuk mengimplementasikan skenario Produser-Consumer.
  • ReaderWriterLockSlim bisa lebih berguna daripada kasus penguncian standar saat pembacaan paralel diharapkan.
  • Keluarga kelas ResetEvent dapat berguna untuk sinkronisasi utas.

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


All Articles