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

Saya menerbitkan artikel asli tentang Habr, yang terjemahannya diposting di blog Codingsight .

Saya terus membuat versi teks dari ceramah saya di pertemuan multithreading. Bagian pertama dapat ditemukan di sini atau di sini , di sana lebih tentang seperangkat alat dasar untuk memulai utas atau Tugas, cara untuk melihat status mereka dan beberapa hal kecil yang manis seperti PLinq. Pada artikel ini saya ingin lebih fokus pada masalah yang mungkin timbul dalam lingkungan multi-threaded dan beberapa cara untuk menyelesaikannya.

Isi





Tentang Sumber Daya yang Dibagikan


Tidak mungkin untuk menulis sebuah program yang akan bekerja di banyak utas, tetapi pada saat yang sama tidak akan memiliki satu sumber daya bersama: bahkan jika itu berhasil pada tingkat abstraksi Anda, kemudian turun satu atau lebih tingkat di bawahnya ternyata masih ada sumber daya yang sama. Saya akan memberikan beberapa contoh:

Contoh # 1:

Khawatir masalah yang mungkin terjadi, Anda membuat utas berfungsi dengan file yang berbeda. Menurut file untuk streaming. Tampaknya bagi Anda bahwa program tidak memiliki satu sumber daya tunggal.

Setelah turun beberapa tingkat di bawah ini, kami memahami bahwa hanya ada satu hard drive, dan driver atau sistem operasinya harus menyelesaikan masalah untuk memastikan akses ke sana.

Contoh # 2:

Setelah membaca contoh # 1, Anda memutuskan untuk meletakkan file pada dua mesin jarak jauh yang berbeda dengan dua potong besi dan sistem operasi yang berbeda secara fisik. Kami menyimpan 2 koneksi berbeda melalui FTP atau NFS.

Setelah turun beberapa tingkat di bawah ini, kami memahami bahwa tidak ada yang berubah, dan driver kartu jaringan atau sistem operasi mesin tempat program dijalankan harus menyelesaikan masalah akses kompetitif.

Contoh # 3:

Setelah kehilangan sebagian besar rambut Anda dalam upaya untuk membuktikan kemungkinan menulis program multi-utas, Anda sepenuhnya menolak file dan menguraikan kalkulasi menjadi dua objek yang berbeda, tautan ke masing-masing yang tersedia hanya untuk satu aliran.

Saya paku selusin paku terakhir ke peti mati dari ide ini: satu runtime dan pengumpul sampah, penjadwal thread, secara fisik satu RAM dan memori, satu prosesor masih berbagi sumber daya.

Jadi, kami menemukan bahwa tidak mungkin untuk menulis program multi-threaded tanpa sumber daya bersama tunggal di semua tingkat abstraksi di seluruh lebar tumpukan teknologi. Untungnya, masing-masing tingkat abstraksi, sebagai suatu peraturan, sebagian atau seluruhnya menyelesaikan masalah akses kompetitif atau hanya melarangnya (contoh: kerangka kerja UI apa pun yang melarang bekerja dengan elemen dari berbagai utas), oleh karena itu masalah yang paling sering muncul adalah sumber daya bersama di tingkat abstraksi Anda. Untuk menyelesaikannya, perkenalkan konsep sinkronisasi.

Kemungkinan masalah saat bekerja di lingkungan multi-utas


Kesalahan dalam perangkat lunak dapat dibagi menjadi beberapa kelompok:

  1. Program tidak membuahkan hasil. Gangguan atau macet.
  2. Program mengembalikan hasil yang salah.
  3. Program menghasilkan hasil yang benar, tetapi tidak memenuhi satu atau beberapa persyaratan non-fungsional lainnya. Berjalan terlalu lama atau menghabiskan terlalu banyak sumber daya.

Dalam lingkungan multi-threaded, dua masalah utama yang menyebabkan kesalahan 1 dan 2 adalah kondisi jalan buntu dan ras .

Jalan buntu


Jalan buntu - jalan buntu. Ada banyak variasi berbeda. Yang paling umum adalah sebagai berikut:



Sementara Thread # 1 sedang melakukan sesuatu, Thread # 2 memblokir sumber B , sedikit kemudian Thread # 1 memblokir sumber A dan mencoba untuk mengunci sumber B , sayangnya ini tidak akan pernah terjadi, karena Thread # 2 akan merilis sumber daya B hanya setelah mengunci sumber daya A.

Kondisi balapan


Race-Condition - kondisi lomba. Situasi di mana perilaku dan hasil perhitungan yang dilakukan oleh program tergantung pada pekerjaan penjadwal thread run-time.
Ketidaknyamanan dari situasi ini justru terletak pada kenyataan bahwa program Anda mungkin tidak berfungsi hanya sekali dari seratus atau bahkan dari satu juta.

Situasi ini diperburuk oleh fakta bahwa masalah dapat berjalan bersamaan, misalnya: dengan perilaku penjadwal ulir tertentu, terjadi kebuntuan.

Selain dua masalah yang mengarah ke kesalahan yang jelas dalam program, ada juga yang mungkin tidak mengarah pada hasil perhitungan yang salah, tetapi lebih banyak waktu atau kekuatan pemrosesan akan dihabiskan untuk mendapatkannya. Dua dari masalah ini adalah: Busy Wait dan Thread Starvation .

Sibuk-tunggu


Sibuk-Tunggu adalah masalah di mana program mengkonsumsi sumber daya prosesor bukan untuk perhitungan, tetapi untuk menunggu.

Seringkali masalah seperti itu dalam kode terlihat seperti ini:

while(!hasSomethingHappened) ; 

Ini adalah contoh kode yang sangat buruk sejak itu Kode seperti itu sepenuhnya menempati satu inti dari prosesor Anda sambil tidak melakukan apa pun yang berguna. Dapat dibenarkan jika dan hanya jika sangat penting untuk memproses perubahan dalam beberapa nilai di utas lainnya. Dan berbicara dengan cepat, saya berbicara tentang kasus ketika Anda tidak bisa menunggu beberapa nanodetik. Dalam kasus lain, yaitu, dalam segala hal yang dapat menghasilkan otak yang sehat, lebih masuk akal untuk menggunakan varietas ResetEvent dan versi Slim-nya. Tentang mereka di bawah ini.

Mungkin salah satu pembaca akan mengusulkan untuk memecahkan masalah memuat satu inti dengan menunggu sia-sia dengan menambahkan konstruksi seperti Thread. Tidur (1) ke loop. Ini benar-benar akan menyelesaikan masalah, tetapi akan membuat yang lain: waktu respons terhadap perubahan akan rata-rata setengah milidetik, yang mungkin tidak banyak, tetapi secara serempak lebih dari yang Anda dapat gunakan primitif sinkronisasi dari keluarga ResetEvent.

Kelaparan benang


Thread-Starvation adalah masalah di mana program memiliki terlalu banyak utas yang bekerja secara bersamaan. Apa artinya sebenarnya aliran yang sibuk dengan perhitungan, dan tidak hanya menunggu respons dari IO mana pun. Dengan masalah ini, semua kemungkinan perolehan kinerja dari menggunakan utas hilang, karena Prosesor menghabiskan banyak waktu untuk mengubah konteks.
Lebih mudah untuk mencari masalah seperti menggunakan berbagai profiler, di bawah ini adalah contoh tangkapan layar dari profiler dotTrace diluncurkan dalam mode Timeline.


(Gambar dapat diklik)

Dalam program yang tidak menderita kelaparan streaming, tidak akan ada warna merah muda pada grafik yang mencerminkan aliran. Selain itu, dalam kategori Subsistem, jelas bahwa 30,6% dari program sedang menunggu CPU.

Ketika masalah seperti itu didiagnosis, itu diselesaikan dengan cukup sederhana: Anda memulai terlalu banyak utas pada satu waktu, memulai lebih sedikit atau tidak sekaligus.

Sinkronkan Alat



Saling bertautan


Ini mungkin cara yang paling ringan untuk melakukan sinkronisasi. Saling bertautan adalah kumpulan operasi atom sederhana. Operasi atom disebut operasi pada saat tidak ada yang bisa terjadi. Dalam .NET, Interlocked diwakili oleh kelas statis dengan nama yang sama dengan sejumlah metode, yang masing-masing mengimplementasikan satu operasi atom.

Untuk mewujudkan kengerian operasi non-atom, cobalah menulis sebuah program yang memulai 10 utas, yang masing-masing menghasilkan sejuta peningkatan dari variabel yang sama, dan pada akhir pekerjaan mereka mencetak nilai dari variabel ini - sayangnya itu akan sangat berbeda dari 10 juta, apalagi Setiap kali program dimulai, akan berbeda. Ini terjadi karena bahkan operasi sederhana seperti kenaikan itu bukan atom, tetapi melibatkan penggalian nilai dari memori, menghitung yang baru, dan menulis kembali. Dengan demikian, dua utas dapat secara bersamaan melakukan masing-masing operasi ini, dalam hal ini kenaikan akan hilang.

Kelas yang saling bertautan menyediakan metode Peningkatan / Penurunan, mudah untuk menebak apa yang mereka lakukan. Mereka nyaman digunakan jika Anda memproses data dalam banyak utas dan mempertimbangkan sesuatu. Kode seperti itu akan bekerja lebih cepat daripada kunci klasik. Jika Saling Bertautan digunakan untuk situasi yang dijelaskan dalam paragraf terakhir, program akan secara stabil memberikan 10 juta dalam situasi apa pun.

Metode CompareExchange melakukan, pada pandangan pertama, fungsi yang agak tidak terlihat, tetapi semua kehadirannya memungkinkan Anda untuk mengimplementasikan banyak algoritma yang menarik, terutama keluarga bebas-kunci.

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

Metode ini mengambil tiga nilai: yang pertama dilewatkan dengan referensi dan ini adalah nilai yang akan diubah ke yang kedua, jika pada saat perbandingan location1 cocok dengan comparand, maka nilai asli location1 akan dikembalikan. Kedengarannya agak membingungkan, karena lebih mudah untuk menulis kode yang melakukan operasi yang sama dengan CompareExchange:

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

Hanya implementasi di kelas Interlocked yang bersifat atomik. Yaitu, jika kita menulis kode seperti itu sendiri, situasi bisa saja terjadi ketika kondisi location1 == comparand sudah terpenuhi, tetapi pada saat ekspresi location1 = nilai dieksekusi, utas lain telah mengubah nilai location1 dan itu akan hilang.

Kita dapat menemukan contoh yang baik menggunakan metode ini dalam kode yang dihasilkan oleh kompiler untuk setiap acara C #.

Mari menulis kelas sederhana dengan satu acara MyEvent:

 class MyClass { public event EventHandler MyEvent; } 

Mari kita membangun proyek dalam konfigurasi Release dan buka perakitan menggunakan dotPeek dengan opsi Show Compiler Generated Code dihidupkan:

 [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 Anda dapat melihat bahwa di balik layar, kompiler menghasilkan algoritma yang agak canggih. Algoritma ini melindungi terhadap situasi kehilangan langganan acara ketika beberapa utas berlangganan acara ini secara bersamaan. Mari kita menulis metode add secara lebih detail, mengingat apa yang dilakukan metode CompareExchange di belakang 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 sudah sedikit lebih jelas, meskipun mungkin masih perlu penjelasan. Dengan kata lain, saya akan menggambarkan algoritma ini sebagai berikut:

Jika MyEvent masih sama seperti pada saat kami mulai menjalankan Delegate.Combine, kemudian tuliskan Delegate.Combine apa yang akan kembali, dan jika tidak, itu tidak masalah, mari kita coba lagi dan ulangi sampai keluar.


Jadi tidak ada langganan acara yang akan hilang. Anda harus menyelesaikan masalah serupa jika tiba-tiba ingin menerapkan larik bebas kunci dinamis yang aman. Jika beberapa aliran terburu-buru untuk menambahkan elemen ke dalamnya, maka penting bahwa mereka semua ditambahkan pada akhirnya.

Monitor. Masukkan, Monitor. Keluar, kunci


Ini adalah desain yang paling umum digunakan untuk sinkronisasi ulir. Mereka menerapkan gagasan bagian kritis: yaitu, kode yang ditulis antara panggilan ke Monitor. Masukkan, Monitor. Keluar pada satu sumber daya dapat dieksekusi pada satu waktu hanya dalam satu utas. Pernyataan kunci adalah gula sintaksis di sekitar panggilan Masuk / Keluar yang dibungkus dalam coba-akhirnya. Fitur yang bagus untuk mengimplementasikan bagian kritis dalam .NET adalah kemampuan untuk memasukkannya kembali ke aliran yang sama. Ini berarti bahwa kode tersebut akan dijalankan tanpa masalah:

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

Tentu saja, tidak mungkin seseorang akan menulis seperti ini, tetapi jika Anda mengoleskan kode ini ke beberapa metode di kedalaman tumpukan-panggilan, fitur ini dapat menghemat beberapa jika. Untuk membuat trik semacam itu menjadi mungkin, para pengembang .NET harus menambahkan batasan - hanya instance dari tipe referensi yang dapat digunakan sebagai objek sinkronisasi, dan beberapa byte secara implisit ditambahkan ke setiap objek di mana pengenal aliran akan ditulis.

Fitur bagian kritis ini dalam c # memberikan satu batasan menarik pada pengoperasian pernyataan kunci: Anda tidak dapat menggunakan pernyataan tunggu di dalam pernyataan kunci. Pada awalnya, itu mengejutkan saya, karena Monitor yang coba-akhirnya serupa. Masukkan / Keluar mengkompilasi. Ada apa? Di sini perlu untuk hati-hati membaca kembali paragraf terakhir sekali lagi, dan kemudian menambahkannya beberapa pengetahuan tentang prinsip async / menunggu: kode setelah menunggu tidak harus dieksekusi pada utas yang sama dengan kode sebelum menunggu, itu tergantung pada konteks sinkronisasi dan keberadaan atau tidak ada panggilan ke ConfigureAwait. Maka Monitor.Exit dapat mengeksekusi pada utas selain Monitor.Enter, yang akan melempar SynchronizationLockException . Jika Anda tidak percaya, maka Anda dapat menjalankan kode berikut di aplikasi konsol: itu akan melempar 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 WinForms atau aplikasi WPF, kode ini akan berfungsi dengan benar jika dipanggil dari utas utama. akan ada konteks sinkronisasi yang mengimplementasikan pengembalian ke UI-Thread setelah menunggu. Bagaimanapun, Anda tidak boleh bermain dengan bagian kritis dalam konteks kode yang berisi operator yang menunggu. Dalam kasus ini, lebih baik menggunakan primitif sinkronisasi, yang akan dibahas nanti.

Berbicara tentang pekerjaan bagian kritis dalam .NET, ada baiknya menyebutkan fitur lain dari implementasinya. Bagian kritis dalam .NET beroperasi dalam dua mode: mode spin-wait dan mode kernel. Algoritma spin-wait direpresentasikan sebagai kode pseudo berikut:

 while(!TryEnter(syncObject)) ; 

Optimalisasi ini ditujukan untuk menangkap bagian kritis tercepat dalam waktu singkat, berdasarkan pada asumsi bahwa jika sumber daya sibuk sekarang, maka akan membebaskan dirinya sendiri. Jika ini tidak terjadi dalam waktu yang singkat, maka utas berjalan menunggu dalam mode kernel, yang, seperti kembali dari itu, membutuhkan waktu. Pengembang .NET telah mengoptimalkan skenario kunci singkat sebanyak mungkin, sayangnya, jika banyak utas mulai merobek bagian penting, ini dapat menyebabkan beban CPU yang tinggi dan tiba-tiba.

SpinLock, SpinWait


Karena saya menyebutkan algoritma spin-wait, ada baiknya menyebutkan BCL SpinLock dan struktur SpinWait. Mereka harus digunakan jika ada alasan untuk percaya bahwa akan selalu ada kesempatan untuk mengambil kunci dengan sangat cepat. Di sisi lain, sulit untuk mengingat tentang mereka sebelum hasil profil menunjukkan bahwa itu adalah penggunaan primitif sinkronisasi lain yang merupakan hambatan dari program Anda.

Monitor. Tunggu, Monitor. Tolong [Semua]


Pasangan metode ini harus dipertimbangkan bersama. Dengan bantuan mereka, berbagai skenario Produsen-Konsumen dapat diimplementasikan.

Produser-Konsumen - pola desain multi-proses / multi-utas dengan asumsi adanya satu atau lebih utas / proses yang menghasilkan data dan satu atau lebih proses / utas yang memproses data ini. Biasanya menggunakan koleksi bersama.

Kedua metode ini hanya dapat dipanggil jika utas yang menyebabkannya memiliki kunci saat ini. Metode Tunggu melepaskan kunci dan menggantung sampai utas lain memanggil Pulse.

Untuk menunjukkan karya itu, saya menulis contoh kecil:

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

(Saya menggunakan gambar, bukan teks, untuk secara visual menunjukkan urutan pelaksanaan instruksi)

Parse: Tetapkan penundaan 100ms pada awal aliran kedua, khusus untuk memastikan bahwa eksekusi dimulai nanti.
- T1: Aliran baris # 2 dimulai
- T1: Aliran baris # 3 memasuki bagian kritis
- T1: Baris # 6 aliran tertidur
- T2: Aliran baris # 3 dimulai
- T2: Baris # 4 membeku sambil menunggu bagian kritis
- T1: Jalur # 7 melepaskan bagian kritis dan membeku sambil menunggu Pulsa keluar
- T2: Baris # 8 memasuki bagian kritis
- T2: Baris # 11 memberi tahu T1 menggunakan metode Pulse
- T2: Baris # 14 keluar dari bagian kritis. Sampai saat itu, T1 tidak dapat melanjutkan eksekusi.
- T1: Baris # 15 bangun
- T1: Baris # 16 meninggalkan bagian kritis

MSDN memiliki komentar penting mengenai penggunaan metode Pulse / Tunggu, yaitu: Monitor tidak menyimpan informasi status, yang berarti bahwa jika metode Pulse dipanggil sebelum metode Tunggu dipanggil, itu dapat menyebabkan kebuntuan. Jika situasi ini memungkinkan, maka 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 pertanyaan tentang kapan harus digunakan. Contoh yang baik adalah implementasi BlockingQueue <T>, di sisi lain, implementasi BlockingCollection <T> dari System.Collections.Concurrent menggunakan SemaphoreSlim untuk sinkronisasi.

ReaderWriterLockSlim


Ini adalah primitif sinkronisasi yang saya cintai, diwakili oleh kelas System.Threading namespace dengan nama yang sama. Menurut saya, banyak program akan bekerja lebih baik jika pengembang mereka menggunakan kelas ini daripada kunci biasa.

Ide: banyak utas dapat membaca, hanya satu tulis. Begitu aliran menyatakan keinginannya untuk menulis, bacaan baru tidak dapat dimulai, tetapi akan menunggu rekaman selesai. Ada juga konsep upgrade-baca-kunci, yang dapat digunakan jika Anda mengerti selama proses membaca bahwa Anda perlu menulis sesuatu, kunci tersebut akan dikonversi menjadi kunci-tulis dalam satu operasi atom.

Ada juga kelas ReadWriteLock di System.Threading namespace, tetapi sangat disarankan untuk pengembangan baru. Versi ramping akan memungkinkan untuk menghindari sejumlah kasus yang menyebabkan kebuntuan, selain itu memungkinkan Anda untuk dengan cepat menangkap kuncinya, karena mendukung sinkronisasi dalam mode spin-wait sebelum berangkat ke mode kernel.

Jika pada saat membaca artikel ini Anda belum tahu tentang kelas ini, maka saya pikir sekarang Anda telah mengingat beberapa contoh dari kode yang ditulis baru-baru ini, di mana pendekatan penguncian akan memungkinkan program untuk bekerja secara efisien.

Antarmuka kelas ReaderWriterLockSlim sederhana dan mudah, tetapi penggunaannya tidak bisa disebut nyaman:

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

Saya suka membungkus penggunaannya di kelas, yang membuatnya menggunakannya jauh lebih nyaman.
Ide: untuk membuat metode Baca / TulisLock yang mengembalikan objek dengan metode Buang, maka ini akan memungkinkan mereka untuk digunakan dalam menggunakan dan dengan jumlah garis itu akan sangat berbeda dari kunci biasa.

 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(); } 

Trik semacam itu memungkinkan Anda untuk menulis lebih jauh:

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


ResetEvent Family


Saya menyertakan kelas ManualResetEvent, ManualResetEventSlim, AutoResetEvent ke keluarga ini.
Kelas ManualResetEvent, versi Slim, dan kelas AutoResetEvent dapat dalam dua status:
- A cocked (non-signaled), dalam keadaan ini, semua utas yang disebut WaitOne membeku hingga acara transisi ke keadaan bersinyal.
- Status diturunkan (ditandai), dalam kondisi ini semua aliran yang tergantung pada panggilan WaitOne dilepaskan. Semua panggilan WaitOne baru pada acara run-down berlalu secara kondisional.

Kelas AutoResetEvent berbeda dari kelas ManualResetEvent di mana secara otomatis memasuki keadaan terkokang setelah melepaskan tepat satu utas. Jika beberapa utas akan menggantung menunggu AutoResetEvent, maka panggilan Setel akan melepaskan hanya satu sewenang-wenang, tidak seperti ManualResetEvent. ManualResetEvent akan 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(); 


Contoh tersebut menunjukkan bahwa acara tersebut masuk ke kondisi terkokang (non-sinyal) secara otomatis hanya dengan melepaskan utas yang tergantung pada panggilan WaitOne.

Kelas ManualResetEvent, tidak seperti ReaderWriterLock, tidak ditandai sebagai usang dan tidak disarankan untuk digunakan setelah penampilan versi Slim-nya. Versi ramping dari kelas ini secara efisien digunakan untuk harapan pendek, seperti Ini terjadi dalam mode Spin-Wait, versi reguler cocok untuk yang lama.

Selain kelas ManualResetEvent dan AutoResetEvent, kelas CountdownEvent juga ada. Kelas ini nyaman untuk implementasi algoritma, di mana bagian yang berhasil diparalelkan diikuti oleh bagian yang menyatukan hasil. Pendekatan ini dikenal sebagai fork-join . Artikel yang luar biasa dikhususkan untuk karya kelas ini, oleh karena itu saya tidak akan menganalisisnya secara rinci di sini.

Kesimpulan


  • Saat bekerja dengan utas, dua masalah yang menghasilkan hasil yang salah atau hilang adalah kondisi balapan dan kebuntuan
  • Masalah-masalah yang menyebabkan program menghabiskan lebih banyak waktu atau sumber daya - utas kelaparan dan kesibukan menunggu
  • .NET kaya akan sinkronisasi utas
  • Ada 2 mode tunggu kunci - Putar Tunggu, Tunggu Inti. Beberapa primitif sinkronisasi thread .NET menggunakan keduanya
  • Saling bertautan adalah seperangkat operasi atom, yang digunakan dalam algoritma bebas kunci, adalah primitif sinkronisasi tercepat
  • Operator kunci dan Monitor. Masukkan / Keluar menerapkan gagasan bagian kritis - sepotong kode yang hanya dapat dieksekusi oleh satu utas pada satu waktu
  • Metode Monitor.Pulse / Wait mudah digunakan untuk mengimplementasikan skrip Produser-Konsumen
  • ReaderWriterLockSlim mungkin lebih efisien daripada skrip kunci biasa di mana pembacaan paralel dapat diterima
  • Keluarga kelas ResetEvent mungkin berguna untuk sinkronisasi utas.

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


All Articles