Kelas utas di Delphi
Raptor[Studio Mental]
http://mental.mentu.com
Empat
Bagian Kritis (CriticalSection) adalah teknologi untuk perlindungan akses data bersama. Ini sebenarnya setara dengan variabel Boolean global. Namun pengoperasiannya berbeda. Ia hanya memiliki dua operasi: Masuk dan Keluar. Kedua statusnya juga dapat dianggap Benar dan Salah, masing-masing menunjukkan apakah ia berada di bagian kritis. Kedua operasi ini juga bersifat primitif, sehingga dapat digunakan untuk melindungi data bersama dari pelanggaran akses dalam aplikasi multi-thread.
Metode penggunaan bagian penting untuk melindungi data bersama sangat sederhana: panggil Enter untuk menyetel tanda bagian penting sebelum mengakses data bersama setiap kali, kemudian operasikan data, dan terakhir panggil Tinggalkan untuk meninggalkan bagian penting. Prinsip perlindungannya adalah sebagai berikut: setelah sebuah thread memasuki bagian kritis, jika thread lain juga ingin mengakses data saat ini, ia akan menemukan bahwa sebuah thread telah memasuki bagian kritis ketika memanggil Enter, dan kemudian thread ini akan menjadi Tutup dan tunggu thread yang saat ini berada di bagian kritis memanggil Tinggalkan untuk meninggalkan bagian kritis. Ketika thread lain menyelesaikan operasi dan memanggil Tinggalkan untuk pergi, thread ini akan dibangunkan, menyetel tanda bagian kritis, dan mulai mengoperasikan data, sehingga mencegah konflik akses.
Mengambil InterlockedIncrement sebelumnya sebagai contoh, kami menggunakan CriticalSection (Windows API) untuk mengimplementasikannya:
Var
InterlockedCrit : TRTLCriticalBagian;
Prosedur Saling BertautanInkrement( var aValue : Integer );
Mulai
EnterCriticalSection(InterlockedCrit);
Inc(Nilai);
LeaveCriticalSection(InterlockedCrit);
Akhir;
Sekarang lihat contoh sebelumnya:
1. Thread A masuk ke critical section (asumsi datanya 3)
2. Thread B masuk ke critical section. Karena A sudah berada di critical section, maka B disuspend.
3. Thread A menambahkan satu ke data (sekarang 4)
4. Thread A meninggalkan bagian kritis dan membangunkan thread B (data di memori sekarang adalah 4)
5. Thread B aktif dan menambahkan satu ke data (sekarang menjadi 5)
6. Thread B meninggalkan bagian kritis, dan data saat ini benar.
Beginilah cara bagian penting melindungi akses ke data bersama.
Mengenai penggunaan critical section, ada satu hal yang perlu diperhatikan: penanganan pengecualian pada saat akses data. Karena jika pengecualian terjadi selama operasi data, operasi Tinggalkan tidak akan dijalankan. Akibatnya, thread yang seharusnya dibangunkan tidak akan dibangunkan, yang dapat menyebabkan program menjadi tidak responsif. Jadi secara umum, pendekatan yang benar adalah dengan menggunakan bagian kritis sebagai berikut:
Masuk ke Bagian Kritis
Mencoba
//Mengoperasikan data bagian penting
Akhirnya
Tinggalkan Bagian Kritis
Akhir;
Hal terakhir yang perlu diperhatikan adalah Event dan CriticalSection keduanya merupakan sumber daya sistem operasi, yang perlu dibuat sebelum digunakan dan dirilis setelah digunakan. Misalnya, Event global: SyncEvent dan CriticalSection global: TheadLock yang digunakan oleh kelas TThread dibuat dan dirilis di InitThreadSynchronization dan DoneThreadSynchronization, dan keduanya dipanggil di unit Inisialisasi dan Finalisasi Kelas.
Karena API digunakan untuk mengoperasikan Event dan CriticalSection di TThread, API digunakan seperti contoh di atas. Faktanya, Delphi telah menyediakan enkapsulasinya di unit SyncObjs, masing-masing adalah kelas TEvent dan kelas TCriticalSection. Cara penggunaannya hampir sama dengan cara penggunaan API sebelumnya. Karena konstruktor TEvent memiliki terlalu banyak parameter, untuk kesederhanaan, Delphi juga menyediakan kelas Event yang diinisialisasi dengan parameter default: TSimpleEvent.
Ngomong-ngomong, izinkan saya memperkenalkan kelas lain yang digunakan untuk sinkronisasi thread: TMultiReadExclusiveWriteSynchronizer, yang didefinisikan dalam unit SysUtils. Sejauh yang saya tahu, ini adalah nama kelas terpanjang yang didefinisikan dalam Delphi RTL. Untungnya, ia memiliki alias pendek: TMREWSync. Untuk kegunaannya, saya rasa Anda sudah bisa mengetahuinya hanya dengan melihat namanya, jadi saya tidak akan bicara lebih banyak.
Dengan pengetahuan persiapan sebelumnya tentang Event dan CriticalSection, kita secara resmi dapat mulai membahas Sinkronisasi dan WaitFor.
Kita tahu bahwa Sinkronisasi mencapai sinkronisasi thread dengan menempatkan bagian kode di thread utama untuk dieksekusi, karena dalam suatu proses, hanya ada satu thread utama. Pertama mari kita lihat penerapan Sinkronisasi:
procedure TThread.Synchronize(Metode: TThreadMethod);
mulai
FSynchronize.FThread := Diri;
FSynchronize.FSynchronizeException := nihil;
FSynchronize.FMethod := Metode;
Sinkronisasi(@FSynchronize);
akhir;
di mana FSynchronize adalah tipe rekaman:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = catatan
FThread: TObject;
Metode FM: Metode TThread;
FSynchronizeException: TObject;
akhir;
Digunakan untuk pertukaran data antara thread dan thread utama, termasuk objek kelas thread yang masuk, metode sinkronisasi dan pengecualian yang terjadi.
Versi yang kelebihan beban disebut dalam Sinkronisasi, dan versi yang kelebihan beban ini cukup istimewa, ini adalah "metode kelas". Yang disebut metode kelas adalah metode anggota kelas khusus. Pemanggilannya tidak memerlukan pembuatan instance kelas, tetapi dipanggil melalui nama kelas seperti konstruktor. Alasan mengapa hal ini diimplementasikan menggunakan metode kelas adalah karena hal ini dapat dipanggil bahkan ketika objek thread tidak dibuat. Namun, dalam praktiknya, versi lain yang kelebihan beban (juga merupakan metode kelas) dan metode kelas lain StaticSynchronize digunakan. Berikut adalah kode untuk Sinkronisasi ini:
prosedur kelas TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
SinkronisasiProc: TSyncProc;
mulai
jika GetCurrentThreadID = MainThreadID maka
Metode ASyncRec.FM
kalau tidak
mulai
SyncProc.Signal := CreateEvent(nihil, Benar, Salah, nihil);
mencoba
EnterCriticalSection(ThreadLock);
mencoba
jika SyncList = nihil maka
SyncList := TList.Buat;
SyncProc.SyncRec := ASyncRec;
SyncList.Tambahkan(@SyncProc);
Acara Sinkronisasi Sinyal;
jika Ditugaskan(WakeMainThread) lalu
WakeMainThread(SyncProc.SyncRec.FThread);
TinggalkanCriticalSection(ThreadLock);
mencoba
WaitForSingleObject(SyncProc.Signal, INFINITE);
Akhirnya
EnterCriticalSection(ThreadLock);
akhir;
Akhirnya
TinggalkanCriticalSection(ThreadLock);
akhir;
Akhirnya
CloseHandle(SyncProc.Sinyal);
akhir;
jika Ditugaskan(ASyncRec.FSynchronizeException) lalu naikkan ASyncRec.FSynchronizeException;
akhir;
akhir;
Kode ini sedikit lebih panjang, namun tidak terlalu rumit.
Yang pertama adalah menentukan apakah thread saat ini adalah thread utama. Jika demikian, cukup jalankan metode sinkronisasi dan kembali.
Jika ini bukan thread utama, maka thread tersebut siap untuk memulai proses sinkronisasi.
Data pertukaran thread (parameter) dan Event Handle dicatat melalui variabel lokal SyncProc. Struktur rekamannya adalah sebagai berikut:
TSyncProc=catatan
SyncRec: PSynchronizeRecord;
Sinyal: Pegangan;
akhir;
Kemudian buat Event, lalu masuk ke bagian kritis (melalui variabel global ThreadLock, karena hanya satu thread yang bisa masuk ke status Sinkronisasi pada saat yang sama, sehingga Anda dapat menggunakan variabel global untuk merekam), lalu simpan data yang direkam di Daftar SyncList (jika ini Jika daftar tidak ada, buatlah). Dapat dilihat bahwa bagian penting dari ThreadLock adalah untuk melindungi akses ke SyncList. Hal ini akan terlihat lagi ketika CheckSynchronize diperkenalkan nanti.
Langkah selanjutnya adalah memanggil SignalSyncEvent. Kodenya telah diperkenalkan saat memperkenalkan konstruktor TThread sebelumnya. Fungsinya adalah untuk sekadar melakukan operasi Set pada SyncEvent. Tujuan SyncEvent ini akan dirinci nanti saat WaitFor diperkenalkan.
Berikutnya adalah bagian terpenting: memanggil event WakeMainThread untuk operasi sinkronisasi. WakeMainThread adalah acara global bertipe TNotifyEvent. Alasan mengapa peristiwa digunakan untuk pemrosesan di sini adalah karena metode Sinkronisasi pada dasarnya menempatkan proses yang perlu disinkronkan ke dalam thread utama untuk dieksekusi melalui pesan. Metode ini tidak dapat digunakan di beberapa aplikasi tanpa loop pesan (seperti Konsol atau DLL). , jadi gunakan acara ini untuk pemrosesan.
Objek aplikasi merespons peristiwa ini. Dua metode berikut digunakan untuk mengatur dan menghapus respons terhadap peristiwa WakeMainThread (dari unit Formulir):
prosedur TApplication.HookSynchronizeWakeup;
mulai
Kelas.WakeMainThread := WakeMainThread;
akhir;
prosedur TApplication.UnhookSynchronizeWakeup;
mulai
Kelas.WakeMainThread := nihil;
akhir;
Dua metode di atas masing-masing dipanggil dalam konstruktor dan destruktor kelas TApplication.
Ini adalah kode yang merespons peristiwa WakeMainThread di objek Aplikasi. Pesan dikirim ke sini.
procedure TApplication.WakeMainThread(Pengirim: TObject);
mulai
PostMessage(Menangani, WM_NULL, 0, 0);
akhir;
Respon pesan ini juga ada pada objek Aplikasi, lihat kode berikut (hapus bagian yang tidak relevan):
procedure TApplication.WndProc(var Pesan: TMessage);
…
mulai
mencoba
…
dengan Pesan lakukan
kasus Pesan dari
…
WM_NULL:
Periksa Sinkronisasi;
…
kecuali
HandleException(Diri);
akhir;
akhir;
Diantaranya, CheckSynchronize juga didefinisikan pada unit Classes, karena relatif kompleks, untuk saat ini kami tidak akan menjelaskannya secara detail, ketahuilah bahwa itu adalah bagian yang khusus menangani fungsi Synchronize kode.
Setelah menjalankan event WakeMainThread, keluar dari bagian kritis, lalu panggil WaitForSingleObject untuk mulai menunggu Acara dibuat sebelum memasuki bagian kritis. Fungsi Event ini adalah menunggu eksekusi metode sinkronisasi ini berakhir. Hal ini akan dijelaskan nanti saat menganalisis CheckSynchronize.
Perhatikan bahwa setelah WaitForSingleObject, Anda masuk kembali ke bagian kritis, tetapi keluar tanpa melakukan apa pun. Tampaknya tidak ada artinya, tetapi itu perlu!
Karena Masuk dan Keluar di bagian kritis harus benar-benar sesuai dengan satu-satu. Jadi bisakah diubah menjadi ini:
jika Ditugaskan(WakeMainThread) lalu
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
Akhirnya
TinggalkanCriticalSection(ThreadLock);
akhir;
Perbedaan terbesar antara kode di atas dan kode aslinya adalah WaitForSingleObject juga termasuk dalam batasan bagian kritis. Tampaknya tidak ada dampaknya, dan ini sangat menyederhanakan kode, tetapi apakah itu benar-benar mungkin?
Faktanya, tidak!
Karena kita tahu setelah bagian Enter Critical, jika thread lain mau masuk lagi maka akan disuspend. Metode WaitFor akan menangguhkan thread saat ini dan tidak akan dibangunkan hingga menunggu SetEvent dari thread lainnya. Jika kode diubah ke atas, jika thread SetEvent juga perlu masuk ke critical section, maka akan terjadi deadlock (untuk teori deadlock silahkan lihat informasi prinsip sistem operasi).
Kebuntuan adalah salah satu aspek terpenting dari sinkronisasi thread!
Akhirnya, Acara yang dibuat di awal dilepaskan. Jika metode tersinkronisasi mengembalikan pengecualian, pengecualian tersebut akan dilempar lagi di sini.
(bersambung)