คลาสเธรดใน Delphi
Raptor[สตูดิโอจิต]
http://mental.mentu.com
สี่
Critical Section (CriticalSection) เป็นเทคโนโลยีสำหรับการป้องกันการเข้าถึงข้อมูลที่ใช้ร่วมกัน ที่จริงแล้วมันเทียบเท่ากับตัวแปรบูลีนโกลบอล แต่การดำเนินการนั้นแตกต่างกัน มีเพียง 2 การดำเนินการเท่านั้น: เข้าและออก สถานะทั้งสองสามารถถือเป็นจริงและเท็จตามลำดับซึ่งบ่งชี้ว่าอยู่ในส่วนวิกฤติหรือไม่ การดำเนินการทั้งสองนี้เป็นการดำเนินการแบบดั้งเดิม ดังนั้นจึงสามารถใช้เพื่อปกป้องข้อมูลที่แชร์จากการละเมิดการเข้าถึงในแอปพลิเคชันแบบมัลติเธรด
วิธีการใช้ส่วนสำคัญในการปกป้องข้อมูลที่แชร์นั้นง่ายมาก: เรียก Enter เพื่อตั้งค่าสถานะส่วนสำคัญก่อนที่จะเข้าถึงข้อมูลที่แชร์ในแต่ละครั้ง จากนั้นดำเนินการข้อมูล และสุดท้ายเรียก Leave เพื่อออกจากส่วนสำคัญ หลักการป้องกันมีดังนี้: หลังจากที่เธรดเข้าสู่ส่วนสำคัญแล้ว หากเธรดอื่นต้องการเข้าถึงข้อมูลในเวลานี้ด้วย จะพบว่าเธรดได้เข้าสู่ส่วนสำคัญแล้วเมื่อเรียก Enter จากนั้นเธรดนี้จะเป็น วางสายและรอให้เธรดอยู่ในส่วนวิกฤติเพื่อเรียกออกจากส่วนสำคัญ เมื่อเธรดอื่นเสร็จสิ้นการดำเนินการและเรียกออกจากเพื่อออก เธรดนี้จะถูกปลุก ตั้งค่าสถานะส่วนสำคัญ และเริ่มข้อมูลการดำเนินงาน จึงป้องกันการเข้าถึงความขัดแย้ง
ยกตัวอย่าง InterlockedIncreation ก่อนหน้านี้ เราใช้ CriticalSection (Windows API) เพื่อนำไปใช้:
วาร์
InterlockedCrit : TRTLCriticalSection;
PROcedure InterlockedIncreation ( var aValue : Integer );
เริ่ม
EnterCriticalSection(InterlockedCrit);
Inc(มูลค่า);
LeaveCriticalSection (InterlockedCrit);
จบ;
ตอนนี้ดูตัวอย่างก่อนหน้านี้:
1. เธรด A เข้าสู่ส่วนวิกฤติ (สมมติว่าข้อมูลคือ 3)
2. เธรด B เข้าสู่ส่วนวิกฤตแล้ว เนื่องจาก A อยู่ในส่วนวิกฤตแล้ว B จึงถูกระงับ
3. เธรด A เพิ่มหนึ่งรายการลงในข้อมูล (ตอนนี้ 4)
4. เธรด A ออกจากส่วนวิกฤติและปลุกเธรด B (ข้อมูลในหน่วยความจำตอนนี้คือ 4)
5. เธรด B ตื่นขึ้นและเพิ่มหนึ่งรายการลงในข้อมูล (ตอนนี้เป็น 5)
6. เธรด B ออกจากส่วนวิกฤติ และข้อมูลปัจจุบันถูกต้อง
นี่คือวิธีที่ส่วนสำคัญปกป้องการเข้าถึงข้อมูลที่แชร์
เกี่ยวกับการใช้ส่วนสำคัญ มีสิ่งหนึ่งที่ควรทราบ: การจัดการข้อยกเว้นระหว่างการเข้าถึงข้อมูล เพราะหากมีข้อยกเว้นเกิดขึ้นระหว่างการดำเนินการข้อมูล การดำเนินการออกจากจะไม่ถูกดำเนินการ ด้วยเหตุนี้ เธรดที่ควรถูกปลุกจะไม่ถูกปลุก ซึ่งอาจทำให้โปรแกรมไม่ตอบสนอง โดยทั่วไปแล้ว แนวทางที่ถูกต้องคือการใช้ส่วนสำคัญดังนี้:
เข้าสู่ส่วนสำคัญ
พยายาม
//ดำเนินการข้อมูลส่วนที่สำคัญ
ในที่สุด
ออกจากCriticalSection
จบ;
สิ่งสุดท้ายที่ควรทราบคือ Event และ CriticalSection เป็นทั้งทรัพยากรระบบปฏิบัติการ ซึ่งจำเป็นต้องสร้างก่อนใช้งานและเผยแพร่หลังการใช้งาน ตัวอย่างเช่น เหตุการณ์ส่วนกลาง: SyncEvent และ CriticalSection ส่วนกลาง: TheadLock ที่ใช้โดยคลาส TThread ถูกสร้างขึ้นและเผยแพร่ใน InitThreadSynchronization และ DoneThreadSynchronization และจะถูกเรียกในการเริ่มต้นและการสิ้นสุดของหน่วยคลาส
เนื่องจาก API ถูกใช้เพื่อดำเนินการ Event และ CriticalSection ใน TThread API จึงถูกใช้เป็นตัวอย่างข้างต้น ที่จริงแล้ว Delphi ได้จัดให้มีการห่อหุ้มพวกมันในหน่วย SyncObjs พวกมันคือคลาส TEvent และคลาส TCriticalSection ตามลำดับ การใช้งานเกือบจะเหมือนกับวิธีการใช้ API ก่อนหน้านี้ เนื่องจากตัวสร้างของ TEvent มีพารามิเตอร์มากเกินไป เพื่อความง่าย Delphi จึงจัดเตรียมคลาสเหตุการณ์ที่เริ่มต้นด้วยพารามิเตอร์เริ่มต้น: TSimpleEvent
ยังไงก็ตาม ให้ฉันแนะนำคลาสอื่นที่ใช้สำหรับการซิงโครไนซ์เธรด: TMultiReadExclusiveWriteSynchronizer ซึ่งกำหนดไว้ในหน่วย SysUtils เท่าที่ฉันรู้ นี่คือชื่อคลาสที่ยาวที่สุดที่กำหนดไว้ใน Delphi RTL โชคดีที่มีนามแฝงแบบสั้น: TMREWSync สำหรับการใช้งาน ฉันคิดว่าคุณสามารถรู้ได้เพียงแค่ดูชื่อ ดังนั้นฉันจะไม่พูดอะไรมากไปกว่านี้
ด้วยความรู้ในการเตรียมการก่อนหน้านี้เกี่ยวกับ Event และ CriticalSection เราสามารถเริ่มพูดคุยเรื่อง Synchronize และ WaitFor อย่างเป็นทางการได้
เรารู้ว่า Synchronize บรรลุการซิงโครไนซ์เธรดโดยการวางส่วนของโค้ดในเธรดหลักเพื่อดำเนินการ เนื่องจากในกระบวนการจะมีเธรดหลักเพียงเธรดเดียวเท่านั้น มาดูการใช้งาน Synchronize กันก่อน:
ขั้นตอน TThread.Synchronize (วิธีการ: TThreadMethod);
เริ่ม
FSynchronize.FThread := ตนเอง;
FSynchronize.FSynchronizeException := ไม่มี;
FSynchronize.FMethod := วิธีการ;
ซิงโครไนซ์(@FSynchronize);
จบ;
โดยที่ FSynchronize เป็นประเภทบันทึก:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = บันทึก
FThread: TObject;
วิธี FM: TThreadMethod;
FSynchronizeException: TObject;
จบ;
ใช้สำหรับการแลกเปลี่ยนข้อมูลระหว่างเธรดและเธรดหลัก รวมถึงอ็อบเจ็กต์คลาสเธรดขาเข้า วิธีการซิงโครไนซ์ และข้อยกเว้นที่เกิดขึ้น
เวอร์ชันที่โอเวอร์โหลดจะถูกเรียกใน Synchronize และเวอร์ชันที่โอเวอร์โหลดนี้ค่อนข้างพิเศษ มันเป็น "วิธีการคลาส" วิธีการเรียนที่เรียกว่าเป็นวิธีการพิเศษของสมาชิกคลาส การร้องขอไม่จำเป็นต้องสร้างอินสแตนซ์ของคลาส แต่ถูกเรียกผ่านชื่อคลาสเหมือนตัวสร้าง เหตุผลที่นำมาใช้โดยใช้เมธอดคลาสก็คือสามารถเรียกได้แม้ว่าจะไม่ได้สร้างอ็อบเจ็กต์เธรดก็ตาม อย่างไรก็ตาม ในทางปฏิบัติ มีการใช้เวอร์ชันโอเวอร์โหลดอื่น (รวมถึงเมธอดคลาสด้วย) และเมธอดคลาสอื่น StaticSynchronize ถูกนำมาใช้ นี่คือรหัสสำหรับการซิงโครไนซ์นี้:
ขั้นตอนคลาส TThread.Synchronize (ASyncRec: PSynchronizeRecord);
var
SyncProc: TSyncProc;
เริ่ม
ถ้า GetCurrentThreadID = MainThreadID แล้ว
ASyncRec.FMethod
อื่น
เริ่ม
SyncProc.Signal := CreateEvent(ไม่มี, จริง, เท็จ, ไม่มี);
พยายาม
EnterCriticalSection(ThreadLock);
พยายาม
ถ้า SyncList = ไม่มีแล้ว
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
เหตุการณ์ SignalSync;
ถ้าได้รับมอบหมาย (WakeMainThread) แล้ว
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection (ThreadLock);
พยายาม
WaitForSingleObject (SyncProc.Signal, INFINITE);
ในที่สุด
EnterCriticalSection(ThreadLock);
จบ;
ในที่สุด
LeaveCriticalSection (ThreadLock);
จบ;
ในที่สุด
CloseHandle(SyncProc.Signal);
จบ;
ถ้าได้รับมอบหมาย (ASyncRec.FSynchronizeException) ให้เพิ่ม ASyncRec.FSynchronizeException;
จบ;
จบ;
รหัสนี้ยาวกว่าเล็กน้อย แต่ก็ไม่ได้ซับซ้อนเกินไป
ประการแรกคือการตรวจสอบว่าเธรดปัจจุบันเป็นเธรดหลักหรือไม่ หากเป็นเช่นนั้น เพียงดำเนินการตามวิธีการซิงโครไนซ์แล้วส่งคืน
หากไม่ใช่เธรดหลัก ก็พร้อมที่จะเริ่มกระบวนการซิงโครไนซ์
ข้อมูลการแลกเปลี่ยนเธรด (พารามิเตอร์) และตัวจัดการเหตุการณ์จะถูกบันทึกผ่านตัวแปรภายในเครื่อง SyncProc โครงสร้างบันทึกจะเป็นดังนี้:
TSyncProc=บันทึก
SyncRec: PSSynchronizeRecord;
สัญญาณ: THandle;
จบ;
จากนั้นสร้างเหตุการณ์ จากนั้นเข้าสู่ส่วนที่สำคัญ (ผ่านตัวแปร ThreadLock ส่วนกลาง เนื่องจากมีเพียงเธรดเดียวเท่านั้นที่สามารถเข้าสู่สถานะซิงโครไนซ์พร้อมกันได้ ดังนั้นคุณจึงใช้ตัวแปรส่วนกลางในการบันทึกได้) จากนั้นจึงจัดเก็บข้อมูลที่บันทึกไว้ใน รายการ SyncList (หากเป็น หากไม่มีรายการ ให้สร้างขึ้นใหม่) จะเห็นได้ว่าส่วนสำคัญของ ThreadLock คือการป้องกันการเข้าถึง SyncList ซึ่งจะเห็นได้อีกครั้งเมื่อมีการแนะนำ CheckSynchronize ในภายหลัง
ขั้นตอนต่อไปคือการเรียก SignalSyncEvent รหัสของมันได้รับการแนะนำเมื่อแนะนำตัวสร้าง TThread ก่อนหน้านี้ หน้าที่ของมันคือเพียงดำเนินการ Set บน SyncEvent วัตถุประสงค์ของ SyncEvent นี้จะมีรายละเอียดในภายหลังเมื่อมีการเปิดตัว WaitFor
ถัดไปคือส่วนที่สำคัญที่สุด: การเรียกเหตุการณ์ WakeMainThread สำหรับการดำเนินการซิงโครไนซ์ WakeMainThread เป็นงานระดับโลกประเภท TNotifyEvent เหตุผลที่ใช้เหตุการณ์ในการประมวลผลที่นี่เนื่องจากวิธีการซิงโครไนซ์ทำให้กระบวนการที่ต้องซิงโครไนซ์ในเธรดหลักเพื่อดำเนินการผ่านข้อความเป็นหลัก ซึ่งไม่สามารถใช้ในบางแอปพลิเคชันโดยไม่มีการวนซ้ำข้อความ (เช่น คอนโซลหรือ DLL) ดังนั้นให้ใช้เหตุการณ์นี้เพื่อการประมวลผล
วัตถุแอปพลิเคชันตอบสนองต่อเหตุการณ์นี้ ใช้สองวิธีต่อไปนี้เพื่อตั้งค่าและล้างการตอบสนองต่อเหตุการณ์ WakeMainThread (จากหน่วยแบบฟอร์ม):
ขั้นตอน TApplication.HookSynchronizeWakeup;
เริ่ม
Classes.WakeMainThread := WakeMainThread;
จบ;
ขั้นตอน TApplication.UnhookSynchronizeWakeup;
เริ่ม
Classes.WakeMainThread := ไม่มี;
จบ;
สองวิธีข้างต้นถูกเรียกในตัวสร้างและตัวทำลายของคลาส TApplication ตามลำดับ
นี่คือรหัสที่ตอบสนองต่อเหตุการณ์ WakeMainThread ในออบเจ็กต์แอปพลิเคชัน ข้อความถูกส่งมาที่นี่ โดยจะใช้ข้อความว่างเพื่อให้บรรลุเป้าหมายนี้:
ขั้นตอน TApplication.WakeMainThread (ผู้ส่ง: TObject);
เริ่ม
PostMessage(จัดการ, WM_NULL, 0, 0);
จบ;
การตอบสนองต่อข้อความนี้ยังอยู่ในวัตถุ Application ดูรหัสต่อไปนี้ (ลบส่วนที่ไม่เกี่ยวข้องออก):
ขั้นตอน TApplication.WndProc (ข้อความ var: TMessage);
-
เริ่ม
พยายาม
-
ด้วยข้อความทำ
กรณีข่าวสารของ
-
WM_NULL:
ตรวจสอบซิงโครไนซ์;
-
ยกเว้น
HandleException (ตนเอง);
จบ;
จบ;
ในหมู่พวกเขา CheckSynchronize ถูกกำหนดไว้ในหน่วย Classes ด้วย เนื่องจากมันค่อนข้างซับซ้อน เราจะไม่อธิบายโดยละเอียดในขณะนี้ เพียงแค่รู้ว่าเป็นส่วนที่จัดการฟังก์ชัน Synchronize โดยเฉพาะ ตอนนี้ดำเนินการวิเคราะห์ต่อไป รหัส.
หลังจากดำเนินการเหตุการณ์ WakeMainThread ให้ออกจากส่วนที่สำคัญ จากนั้นเรียก WaitForSingleObject เพื่อเริ่มรอเหตุการณ์ที่สร้างขึ้นก่อนที่จะเข้าสู่ส่วนที่สำคัญ ฟังก์ชั่นของเหตุการณ์นี้คือรอให้การดำเนินการวิธีการซิงโครไนซ์นี้สิ้นสุดลง ซึ่งจะอธิบายในภายหลังเมื่อวิเคราะห์ CheckSynchronize
โปรดทราบว่าหลังจาก WaitForSingleObject คุณจะเข้าสู่ส่วนที่สำคัญอีกครั้ง แต่ออกโดยไม่ทำอะไรเลย ดูเหมือนไม่มีความหมาย แต่จำเป็น!
เพราะการเข้าและออกในส่วนสำคัญจะต้องสอดคล้องกันแบบหนึ่งต่อหนึ่งอย่างเคร่งครัด จึงสามารถเปลี่ยนเป็นสิ่งนี้ได้:
ถ้าได้รับมอบหมาย (WakeMainThread) แล้ว
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject (SyncProc.Signal, INFINITE);
ในที่สุด
LeaveCriticalSection (ThreadLock);
จบ;
ความแตกต่างที่ใหญ่ที่สุดระหว่างโค้ดด้านบนและโค้ดต้นฉบับคือ WaitForSingleObject ยังรวมอยู่ในข้อจำกัดของส่วนวิกฤติด้วย ดูเหมือนว่าจะไม่มีผลกระทบใด ๆ และทำให้โค้ดง่ายขึ้นอย่างมาก แต่เป็นไปได้จริงหรือ?
ที่จริงแล้วไม่!
เพราะเรารู้ว่าหลังจากเข้าสู่ส่วนวิกฤติแล้ว หากเธรดอื่นต้องการเข้าอีกครั้ง เธรดเหล่านั้นจะถูกระงับ วิธีการ WaitFor จะระงับเธรดปัจจุบัน และจะไม่ถูกปลุกจนกว่าจะรอ SetEvent ของเธรดอื่น ถ้ารหัสถูกเปลี่ยนแปลงไปข้างต้น ถ้าเธรด SetEvent ยังจำเป็นต้องเข้าสู่ส่วนที่สำคัญ การหยุดชะงักจะเกิดขึ้น (สำหรับทฤษฎีของการหยุดชะงัก โปรดดูข้อมูลเกี่ยวกับหลักการของระบบปฏิบัติการ)
การหยุดชะงักเป็นหนึ่งในส่วนที่สำคัญที่สุดของการซิงโครไนซ์เธรด!
ในที่สุด เหตุการณ์ที่สร้างขึ้นเมื่อเริ่มต้นจะถูกปล่อยออกมา หากวิธีการซิงโครไนซ์ส่งคืนข้อยกเว้น ข้อยกเว้นจะถูกส่งอีกครั้งที่นี่
(จะดำเนินต่อไป)