Setiap programmer yang menggunakan lebih dari satu utas dalam programnya telah mengalami sinkronisasi primitif. Dalam konteks .NET, ada banyak dari mereka, saya tidak akan mencantumkannya,
MSDN telah melakukan ini untuk saya.
Saya harus menggunakan banyak dari primitif ini, dan mereka dengan sempurna membantu mengatasi tugas. Tetapi dalam artikel ini saya ingin berbicara tentang kunci biasa dalam aplikasi desktop dan bagaimana primitif baru (setidaknya untuk saya) muncul, yang dapat disebut PriorityLock.
Masalah
Ketika mengembangkan aplikasi multithreaded yang sangat, seorang manajer muncul di suatu tempat yang memproses utas yang tak terhitung jumlahnya. Begitu juga dengan saya. Dan manajer ini berhasil, memproses banyak permintaan dari ratusan utas. Dan semuanya baik-baik saja dengannya, tetapi di dalam kunci biasa berfungsi.
Dan kemudian suatu hari seorang pengguna (misalnya, saya) mengklik tombol di antarmuka aplikasi, aliran terbang ke manajer (bukan aliran UI tentu saja) dan mengharapkan untuk melihat penerimaan yang super ramah, tetapi sebaliknya, ia bertemu dengan Bibi Klava dari penerimaan klinik paling padat di klinik paling padat dengan kata-kata “Saya tidak peduli siapa yang mengarahkan Anda. Saya punya 950 lebih seperti Anda. Pergi dan dapatkan mereka. Saya tidak peduli bagaimana Anda mengetahuinya. " Ini adalah cara kerja kunci di .NET. Dan semuanya tampak baik-baik saja, semuanya akan dieksekusi dengan benar, tetapi pengguna jelas tidak berencana untuk menunggu beberapa detik untuk menanggapi tindakannya.
Di sinilah kisah memilukan berakhir dan solusi untuk masalah teknis dimulai.
Solusi
Setelah mempelajari primitif standar, saya tidak menemukan opsi yang cocok. Oleh karena itu, saya memutuskan untuk menulis kunci saya, yang akan memiliki entri standar dan prioritas tinggi. Ngomong-ngomong, setelah menulis, aku belajar nuget juga, aku tidak menemukan yang seperti itu di sana, walaupun aku mungkin mencari dengan buruk.
Untuk menulis primitif seperti itu (atau tidak lagi primitif) saya membutuhkan operasi SemaphoreSlim, SpinWait dan Interlocked. Di spoiler, saya mengutip versi pertama PriorityLock saya (hanya kode sinkron, tetapi itu yang paling penting), dan penjelasan untuk itu.
Teks tersembunyiDalam hal sinkronisasi, tidak ada penemuan, saat seseorang terkunci, orang lain tidak bisa masuk. Jika prioritas tinggi telah datang, itu didorong ke depan oleh semua orang yang menunggu prioritas rendah.
Kelas LockMgr, diusulkan untuk bekerja dengannya dalam kode Anda. Dialah yang menjadi objek utama sinkronisasi. Membuat objek Locker dan HighLocker, berisi semaphores, SpinWait's, penghitung yang ingin masuk ke bagian kritis, utas saat ini, dan rekursi rekursi.
public class LockMgr { internal int HighCount; internal int LowCount; internal Thread CurThread; internal int RecursionCount; internal readonly SemaphoreSlim Low = new SemaphoreSlim(1); internal readonly SemaphoreSlim High = new SemaphoreSlim(1); internal SpinWait LowSpin = new SpinWait(); internal SpinWait HighSpin = new SpinWait(); public Locker HighLock() { return new HighLocker(this); } public Locker Lock(bool high = false) { return new Locker(this, high); } }
Kelas Locker mengimplementasikan antarmuka IDisposable. Untuk menerapkan rekursi saat mengambil kunci, kita mengingat Id dari stream, lalu memeriksanya. Selanjutnya, tergantung pada prioritas, dalam hal prioritas tinggi, kami segera mengatakan bahwa kami tiba (menambah penghitung HighCount), mendapatkan semafor Tinggi, dan menunggu (jika perlu) untuk melepaskan kunci dari prioritas rendah, setelah itu kami siap untuk mendapatkan kunci. Dalam hal prioritas rendah, semaphore rendah didapat, maka kami menunggu penyelesaian semua arus prioritas tinggi, dan, dengan mengambil semaphore tinggi sebentar, tambahkan LowCount.
Perlu disebutkan bahwa arti dari HighCount dan LowCount berbeda, HighCount menampilkan jumlah utas prioritas yang datang ke kunci, ketika LowCount hanya berarti bahwa utas (satu tunggal) dengan prioritas rendah masuk ke kunci.
public class Locker : IDisposable { private readonly bool _isHigh; private LockMgr _mgr; public Locker(LockMgr mgr, bool isHigh = false) { _isHigh = isHigh; _mgr = mgr; if (mgr.CurThread == Thread.CurrentThread) { mgr.RecursionCount++; return; } if (_isHigh) { Interlocked.Increment(ref mgr.HighCount); mgr.High.Wait(); while (Interlocked.CompareExchange(ref mgr.LowCount, 0, 0) != 0) mgr.HighSpin.SpinOnce(); } else { mgr.Low.Wait(); while (Interlocked.CompareExchange(ref mgr.HighCount, 0, 0) != 0) mgr.LowSpin.SpinOnce(); try { mgr.High.Wait(); Interlocked.Increment(ref mgr.LowCount); } finally { mgr.High.Release(); } } mgr.CurThread = Thread.CurrentThread; } public void Dispose() { if (_mgr.RecursionCount > 0) { _mgr.RecursionCount--; _mgr = null; return; } _mgr.RecursionCount = 0; _mgr.CurThread = null; if (_isHigh) { _mgr.High.Release(); Interlocked.Decrement(ref _mgr.HighCount); } else { _mgr.Low.Release(); Interlocked.Decrement(ref _mgr.LowCount); } _mgr = null; } } public class HighLocker : Locker { public HighLocker(LockMgr mgr) : base(mgr, true) { } }
Menggunakan objek kelas LockMgr sangat ringkas. Contoh ini dengan jelas menunjukkan kemungkinan menggunakan kembali _lockMgr di dalam bagian kritis, sementara prioritas tidak lagi penting.
private PriorityLock.LockMgr _lockMgr = new PriorityLock.LockMgr(); public void LowPriority() { using (_lockMgr.Lock()) { using (_lockMgr.HighLock()) {
Jadi saya memecahkan masalah saya. Pemrosesan aksi pengguna mulai dilakukan dengan prioritas tinggi, tidak ada yang terluka, semua orang menang.
Sinkronisasi
Karena objek dari kelas SemaphoreSlim mendukung penantian asinkron, saya juga menambahkan kesempatan ini untuk diri saya sendiri. Kode berbeda minimal dan pada akhir artikel saya akan memberikan tautan ke kode sumber.
Penting untuk dicatat di sini bahwa Tugas tidak dilampirkan ke utas dengan cara apa pun, oleh karena itu, penggunaan kembali asinkron dari kunci tidak dapat dilaksanakan dengan cara yang serupa. Selain itu, properti
Task.CurrentId seperti yang dijelaskan oleh MSDN tidak menjamin apa pun. Di sinilah opsi saya berakhir.
Dalam mencari solusi, saya menemukan proyek
NeoSmart.AsyncLock , dalam deskripsi yang mendukung untuk menggunakan kembali kunci asinkron ditunjukkan. Secara teknis, menggunakan kembali bekerja. Namun sayangnya kunci itu sendiri bukan kunci. Hati-hati jika Anda menggunakan paket ini, ketahuilah bahwa BUKAN itu berfungsi dengan benar!
Kesimpulan
Hasilnya adalah kelas yang mendukung operasi sinkron dengan penggunaan kembali, dan operasi asinkron tanpa menggunakan kembali. Operasi asinkron dan sinkron dapat digunakan berdampingan, tetapi tidak dapat digunakan bersama! Semua karena kurangnya dukungan untuk menggunakan kembali opsi asinkron.
Saya harap saya tidak sendirian dalam masalah seperti itu dan solusi saya akan berguna bagi seseorang. Saya memposting perpustakaan di github dan nuget.
Ada tes di repositori yang menunjukkan kesehatan PriorityLock. Pada bagian asinkron pengujian ini, NeoSmart.AsyncLock diuji, dan pengujian gagal.
Tautan ke nugetTautan Github