คลาสเธรดใน Delphi
Raptor[สตูดิโอจิต]
http://mental.mentu.com
ส่วนที่ 2
อย่างแรกคือตัวสร้าง:
ตัวสร้าง TThread.Create (CreateSuspended: Boolean);
เริ่ม
สืบทอดสร้าง;
เพิ่มเธรด;
FSuspended := สร้างระงับ;
FCreateSuspended := สร้าง Suspended;
FHandle := BeginThread (ไม่มี, 0, @ThreadPRoc, ตัวชี้ (ตนเอง), CREATE_SUSPENDED, FThreadID);
ถ้า FHandle = 0 แล้ว
เพิ่ม EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
จบ;
แม้ว่าตัวสร้างนี้จะมีโค้ดไม่มากนัก แต่ก็ถือได้ว่าเป็นสมาชิกที่สำคัญที่สุดเนื่องจากเธรดถูกสร้างขึ้นที่นี่
หลังจากเรียก TObject.Create ผ่าน Inherited ประโยคแรกคือการเรียกกระบวนการ: AddThread และซอร์สโค้ดจะเป็นดังนี้:
ขั้นตอน AddThread;
เริ่ม
การเพิ่มขึ้นแบบประสานกัน (ThreadCount);
จบ;
นอกจากนี้ยังมี RemoveThread ที่เกี่ยวข้องด้วย:
ขั้นตอน RemoveThread;
เริ่ม
InterlockedDecreation (จำนวนเธรด);
จบ;
ฟังก์ชันของพวกเขานั้นง่ายมาก ซึ่งก็คือการนับจำนวนเธรดในกระบวนการโดยการเพิ่มหรือลดตัวแปรโกลบอล เพียงแต่ว่ากระบวนการ Inc/Dec ที่ใช้กันทั่วไปไม่ได้ใช้เพื่อเพิ่มหรือลดตัวแปรที่นี่ แต่มีการใช้กระบวนการ InterlockedIncreation/InterlockedDecreation ทั้งคู่ โดยจะใช้ฟังก์ชันเดียวกันทุกประการ ทั้งการเพิ่มหรือการลบหนึ่งรายการให้กับตัวแปร แต่มีความแตกต่างที่ใหญ่ที่สุดประการหนึ่ง นั่นก็คือ InterlockedIncreation/InterlockedDecreation นั้นปลอดภัยสำหรับเธรด นั่นคือพวกเขาสามารถรับประกันผลลัพธ์การดำเนินการที่ถูกต้องภายใต้มัลติเธรด แต่ Inc/Dec ไม่สามารถทำได้ หรือในแง่ทฤษฎีระบบปฏิบัติการ นี่คือคู่ของการดำเนินการ "ดั้งเดิม"
ใช้บวกหนึ่งเป็นตัวอย่างเพื่อแสดงความแตกต่างในรายละเอียดการใช้งานระหว่างทั้งสอง:
โดยทั่วไปแล้ว การดำเนินการเพิ่มข้อมูลหนึ่งลงในข้อมูลหน่วยความจำจะมีขั้นตอนสามขั้นตอนหลังจากการสลาย:
1. อ่านข้อมูลจากหน่วยความจำ
2. ข้อมูลบวกหนึ่ง
3. เก็บไว้ในหน่วยความจำ
ตอนนี้สมมติสถานการณ์ที่อาจเกิดขึ้นเมื่อใช้ Inc เพื่อดำเนินการเพิ่มในแอปพลิเคชันแบบสองเธรด:
1. Thread A อ่านข้อมูลจากหน่วยความจำ (สมมุติว่าเป็น 3)
2. เธรด B อ่านข้อมูลจากหน่วยความจำ (เช่น 3)
3. เธรด A เพิ่มหนึ่งรายการลงในข้อมูล (ตอนนี้คือ 4)
4. เธรด B เพิ่มหนึ่งรายการลงในข้อมูล (ตอนนี้ก็เป็น 4 ด้วย)
5. เธรด A เก็บข้อมูลลงในหน่วยความจำ (ข้อมูลในหน่วยความจำตอนนี้คือ 4)
6. เธรด B ยังเก็บข้อมูลไว้ในหน่วยความจำ (ข้อมูลในหน่วยความจำยังคงเป็น 4 แต่ทั้งสองเธรดได้เพิ่มหนึ่งรายการเข้าไป ซึ่งควรจะเป็น 5 ดังนั้นจึงมีผลลัพธ์ที่ไม่ถูกต้องที่นี่)
ไม่มีปัญหาดังกล่าวกับกระบวนการ InterlockIncreation เนื่องจากสิ่งที่เรียกว่า "ดั้งเดิม" เป็นการดำเนินการอย่างต่อเนื่อง นั่นคือ ระบบปฏิบัติการสามารถรับประกันได้ว่าการสลับเธรดจะไม่เกิดขึ้นก่อนที่จะดำเนินการ "ดั้งเดิม" ดังนั้นในตัวอย่างข้างต้น หลังจากที่เธรด A ดำเนินการและจัดเก็บข้อมูลในหน่วยความจำเสร็จแล้ว เธรด B จะสามารถเริ่มดึงหมายเลขและเพิ่มหมายเลขได้ ซึ่งจะทำให้แน่ใจได้ว่าแม้ในสถานการณ์แบบมัลติเธรด ผลลัพธ์จะเหมือนเดิม ถูกต้อง.
ตัวอย่างก่อนหน้านี้ยังแสดงให้เห็นถึงสถานการณ์ "ความขัดแย้งในการเข้าถึงเธรด" ซึ่งเป็นสาเหตุที่ทำให้เธรดจำเป็นต้อง "ซิงโครไนซ์" (ซิงโครไนซ์) ซึ่งจะกล่าวถึงในรายละเอียดในภายหลังเมื่อมีการกล่าวถึงการซิงโครไนซ์
เมื่อพูดถึงการซิงโครไนซ์ มีการพูดนอกเรื่อง: หลี่หมิงศาสตราจารย์แห่งมหาวิทยาลัยวอเตอร์ลูในแคนาดาเคยคัดค้านคำว่าซิงโครไนซ์ซึ่งแปลว่า "ซิงโครไนซ์" ใน "การซิงโครไนซ์เธรด" โดยส่วนตัวแล้วฉันคิดว่าสิ่งที่เขาพูดนั้นจริงมาก มีเหตุผล. "การซิงโครไนซ์" ในภาษาจีนหมายถึง "เกิดขึ้นในเวลาเดียวกัน" และจุดประสงค์ของ "การซิงโครไนซ์เธรด" ก็เพื่อหลีกเลี่ยงไม่ให้ "เกิดขึ้นในเวลาเดียวกัน" ในภาษาอังกฤษ Synchronize มีความหมาย 2 ความหมาย ความหมายหนึ่งคือการซิงโครไนซ์ในความหมายดั้งเดิม (เพื่อให้เกิดขึ้นในเวลาเดียวกัน) และอีกความหมายหนึ่งคือ "To Operate in unison" (To Operate in unison) คำว่า Synchronize ใน "thread synchronization" ควรอ้างอิงถึงความหมายหลัง นั่นคือ "เพื่อให้แน่ใจว่าหลาย threads รักษาการประสานงานและหลีกเลี่ยงข้อผิดพลาดเมื่อเข้าถึงข้อมูลเดียวกัน" อย่างไรก็ตาม ยังมีคำแปลที่ไม่ถูกต้องเช่นนี้อีกมากในอุตสาหกรรมไอที เนื่องจากกลายเป็นแบบแผน บทความนี้จึงขออธิบายต่อไปในที่นี้ เพราะการพัฒนาซอฟต์แวร์เป็นงานที่ต้องใช้ความพิถีพิถัน และสิ่งที่ควรชี้แจง ต้องไม่คลุมเครือ
กลับไปที่ Constructor ของ TThread กัน สิ่งที่สำคัญที่สุดต่อไปคือประโยคนี้:
FHandle := BeginThread (ไม่มี, 0, @ThreadProc, ตัวชี้ (ตนเอง), CREATE_SUSPENDED, FThreadID);
มีการใช้ฟังก์ชัน Delphi RTL BeginThread ที่กล่าวถึงก่อนหน้านี้ มีพารามิเตอร์หลายตัว โดยพารามิเตอร์หลักคือพารามิเตอร์ตัวที่สามและสี่ พารามิเตอร์ตัวที่สามคือฟังก์ชันเธรดที่กล่าวถึงก่อนหน้านี้ นั่นคือส่วนของโค้ดที่ดำเนินการในเธรด พารามิเตอร์ที่สี่คือพารามิเตอร์ที่ส่งผ่านไปยังฟังก์ชันเธรด นี่คือวัตถุเธรดที่สร้างขึ้น (เช่น ตนเอง) ในบรรดาพารามิเตอร์อื่น ๆ พารามิเตอร์ที่ห้าใช้เพื่อตั้งค่าเธรดให้หยุดชั่วคราวหลังจากการสร้างและไม่ได้ดำเนินการทันที (งานในการเริ่มต้นเธรดจะถูกกำหนดตามแฟล็ก CreateSuspended ใน AfterConstruction) และประการที่หกคือการส่งคืน ID เธรด
ตอนนี้เรามาดูแกนหลักของ TThread: ฟังก์ชันเธรด ThreadProc สิ่งที่น่าสนใจคือแกนกลางของคลาสเธรดนี้ไม่ใช่สมาชิกของเธรด แต่เป็นฟังก์ชันโกลบอล (เนื่องจากรูปแบบพารามิเตอร์ของกระบวนการ BeginThread สามารถใช้ฟังก์ชันโกลบอลได้เท่านั้น) นี่คือรหัส:
ฟังก์ชั่น ThreadProc (เธรด: TThread): จำนวนเต็ม;
var
FreeThread: บูลีน;
เริ่ม
พยายาม
ถ้าไม่ใช่ Thread.Terminated แล้ว
พยายาม
เธรดดำเนินการ;
ยกเว้น
Thread.FFatalException := AcquireExceptionObject;
จบ;
ในที่สุด
FreeThread := Thread.FFreeOnTerminate;
ผลลัพธ์ := Thread.FReturnValue;
Thread.DoTerminate;
Thread.FFinished := True;
เหตุการณ์ SignalSync;
ถ้า FreeThread แล้ว Thread.Free;
EndThread(ผลลัพธ์);
จบ;
จบ;
แม้ว่าจะมีโค้ดไม่มากนัก แต่เป็นส่วนที่สำคัญที่สุดของ TThread ทั้งหมด เนื่องจากโค้ดนี้เป็นโค้ดที่รันจริงในเธรด ต่อไปนี้เป็นคำอธิบายโค้ดทีละบรรทัด:
ขั้นแรก ให้กำหนดสถานะสิ้นสุดของคลาสเธรด หากไม่ได้ทำเครื่องหมายว่าสิ้นสุด ให้เรียกวิธีดำเนินการของคลาสเธรดเพื่อดำเนินการโค้ดเธรด เนื่องจาก TThread เป็นคลาสนามธรรม และวิธีการดำเนินการเป็นวิธีนามธรรม โดยพื้นฐานแล้ว รันโค้ด Execute ในคลาสที่ได้รับ
ดังนั้น Execute จึงเป็นฟังก์ชันเธรดในคลาสเธรด โค้ดทั้งหมดใน Execute จำเป็นต้องถือเป็นโค้ดเธรด เช่น การป้องกันข้อขัดแย้งในการเข้าถึง
ถ้ามีข้อยกเว้นเกิดขึ้นในการดำเนินการ วัตถุข้อยกเว้นจะได้รับผ่าน AcquireExceptionObject และเก็บไว้ในสมาชิก FFatalException ของคลาสเธรด
ในที่สุดก็ยังมีการตกแต่งขั้นสุดท้ายก่อนที่ด้ายจะสิ้นสุด ตัวแปรภายในเครื่อง FreeThread จะบันทึกการตั้งค่าของแอตทริบิวต์ FreeOnTerminated ของคลาสเธรด จากนั้นตั้งค่าการส่งคืนเธรดเป็นค่าของแอตทริบิวต์ค่าที่ส่งคืนของคลาสเธรด จากนั้นดำเนินการวิธี DoTerminate ของคลาสเธรด
รหัสสำหรับวิธีการDoTerminateจะเป็นดังนี้:
ขั้นตอน TThread.DoTerminate;
เริ่ม
ถ้าได้รับมอบหมาย (FOnTerminate) ให้ซิงโครไนซ์ (CallOnTerminate);
จบ;
ง่ายมาก เพียงเรียกเมธอด CallOnTerminate ผ่าน Synchronize และโค้ดของเมธอด CallOnTerminate จะเป็นดังนี้ ซึ่งก็คือเรียกเหตุการณ์ OnTerminate เพียงอย่างเดียว:
ขั้นตอน TThread.CallOnTerminate;
เริ่ม
ถ้าได้รับมอบหมาย (FOnTerminate) แล้ว FOnTerminate (ตนเอง);
จบ;
เนื่องจากเหตุการณ์ OnTerminate ถูกดำเนินการในการซิงโครไนซ์ โดยพื้นฐานแล้วไม่ใช่โค้ดเธรด แต่เป็นโค้ดเธรดหลัก (ดูการวิเคราะห์ของ Synchronize ในภายหลังสำหรับรายละเอียด)
หลังจากดำเนินการ OnTerminate ให้ตั้งค่าสถานะ FFinished ของคลาสเธรดเป็น True
จากนั้น กระบวนการ SignalSyncEvent จะถูกดำเนินการ และโค้ดจะเป็นดังนี้:
ขั้นตอน SignalSyncEvent;
เริ่ม
เซ็ตอีเว้นท์(SyncEvent);
จบ;
ง่ายมาก เพียงตั้งค่า Global Event: SyncEvent บทความนี้จะอธิบายการใช้ Event โดยละเอียดในภายหลัง และวัตถุประสงค์ของ SyncEvent จะมีการอธิบายในกระบวนการ WaitFor
จากนั้น จะมีการตัดสินใจว่าจะปล่อยคลาสเธรดตามการตั้งค่า FreeOnTerminate ที่บันทึกไว้ใน FreeThread หรือไม่ เมื่อคลาสเธรดถูกรีลีส จะมีการดำเนินการบางอย่าง โปรดดูรายละเอียดการใช้งาน destructor ต่อไปนี้
ในที่สุด EndThread ถูกเรียกให้สิ้นสุดเธรดและส่งคืนค่าเธรด
ณ จุดนี้ เธรดสิ้นสุดลงอย่างสมบูรณ์แล้ว
(จะดำเนินต่อไป)