Signaling: การส่งสัญญาณข้าม Thread ด้วย Event Wait Handles

1. 🎯 ตอนที่ 18: Signaling: การส่งสัญญาณข้าม Thread ด้วย Event Wait Handles
2. 📖 เปิดฉาก (The Hook)
สวัสดีครับผู้อ่านทุกท่าน! กลับมาพบกันอีกครั้งในซีรีส์ เจาะลึก C# Concurrency & Multithreading
ในตอนที่ผ่านๆ มา เราได้เรียนรู้วิธีการใช้ lock, Mutex, Semaphore รวมถึง ReaderWriterLockSlim ซึ่งเป็นกลไกที่ใช้สำหรับ “ปกป้อง” ข้อมูล (Shared State) ไม่ให้พังเมื่อมีหลาย Thread เข้ามารุมทึ้งพร้อมกัน
แต่วันนี้เราจะเปลี่ยนมุมมองครับ ลองจินตนาการถึงเหตุการณ์ที่คุณสร้าง Worker Thread ขึ้นมาเพื่อประมวลผลข้อมูล แต่ข้อมูลนั้นยังเดินทางมาไม่ถึง (เช่น รอโหลดไฟล์จากอินเทอร์เน็ต) ถ้าคุณเขียนโค้ดแบบ while(!isDataReady) { } เพื่อเช็คสถานะวนไปเรื่อยๆ (Polling) สิ่งที่เกิดขึ้นคือ Thread นั้นจะกินพลังงาน CPU ไปฟรีๆ 100% โดยไม่ได้งานอะไรเลย!
ในทางวิศวกรรมซอฟต์แวร์ เราแก้ปัญหานี้ด้วยกลไกที่เรียกว่า “Signaling” หรือการส่งสัญญาณแจ้งเตือนข้าม Thread ครับ แนวคิดคือ “ถ้างานยังไม่มา ก็ให้ Thread นอนหลับ (Block) ไปซะ พอของมาถึง ค่อยปลุกให้ตื่น” และอุปกรณ์ที่เราจะมาเจาะลึกกันในวันนี้ก็คือคลาสในตระกูล Event Wait Handles นั่นเองครับ!
3. 🧠 แก่นวิชา (Core Concepts)
กลไก Signaling ใน C# อาศัยคลาสพื้นฐานที่ชื่อว่า EventWaitHandle (ซึ่งไม่ได้มีความเกี่ยวข้องใดๆ กับคีย์เวิร์ด event ของ C# นะครับ) โดยแบ่งออกเป็น 2 รูปแบบหลักๆ ซึ่งเปรียบเทียบให้เห็นภาพได้ดังนี้ครับ:
AutoResetEvent(ประตูกั้นตั๋วรถไฟฟ้า - The Ticket Turnstile):- มันทำงานเหมือนประตูกั้นที่สถานีรถไฟฟ้าครับ การหยอดเหรียญ 1 ครั้ง (เรียกคำสั่ง
Set()) จะอนุญาตให้คน (Thread) เดินผ่านไปได้ “แค่ 1 คนเท่านั้น” - คำว่า “Auto” หมายความว่า ทันทีที่มี Thread เดินผ่านประตูไป ประตูนั้นจะปิดหรือรีเซ็ตตัวเอง (Reset) กลับมาล็อกอัตโนมัติทันที
- เมื่อมีหลาย Thread มารอที่ประตู พวกมันจะเข้าคิวรอทีละตัวครับ
- มันทำงานเหมือนประตูกั้นที่สถานีรถไฟฟ้าครับ การหยอดเหรียญ 1 ครั้ง (เรียกคำสั่ง
ManualResetEvent(ประตูรั้วฟาร์มเปิดอ้า - The Corral Gate):- มันทำงานเหมือนประตูรั้วฟาร์มม้าครับ (Corral gate) เมื่อประตูถูกล็อก Thread ทุกตัวที่มาถึงจะต้องยืนรอ (เรียก
WaitOne()) - แต่เมื่อไหร่ที่คุณเปิดประตู (เรียกคำสั่ง
Set()) Thread ทุกตัวที่รออยู่จะแห่กันวิ่งผ่านประตูไปได้ทั้งหมดพร้อมๆ กัน! - ประตูจะเปิดอ้าค้างอยู่อย่างนั้น จนกว่าคุณจะสั่งปิดมันด้วยตัวเอง (เรียกคำสั่ง
Reset())
- มันทำงานเหมือนประตูรั้วฟาร์มม้าครับ (Corral gate) เมื่อประตูถูกล็อก Thread ทุกตัวที่มาถึงจะต้องยืนรอ (เรียก
- การกำเนิดร่าง Slim (
ManualResetEventSlim):- ในยุค .NET 4.0 Microsoft ได้เปิดตัว
ManualResetEventSlimซึ่งเป็นร่างอัปเกรดที่ใช้เทคนิค Spinning เข้ามาช่วย ทำให้มันเบา (Lightweight) และทำงานเร็วกว่าตัวธรรมดาอย่างมากสำหรับการรอช่วงสั้นๆ โดยไม่ต้องพึ่งพากลไกระดับ OS ทันที
- ในยุค .NET 4.0 Microsoft ได้เปิดตัว

4. 💻 ร่ายมนต์โค้ด (Show me the Code)
ลองมาดูตัวอย่างการใช้ ManualResetEventSlim เพื่อให้ Thread หลัก สั่ง “ปล่อยตัว” Worker Threads หลายๆ ตัวให้เริ่มทำงานพร้อมกัน เหมือนการยิงปืนปล่อยตัวนักวิ่งครับ
using System;
using System.Threading;
using System.Threading.Tasks;
public class SignalingDemo
{
// สร้างประตูรั้วแบบ Manual (false หมายถึง ประตูปิดอยู่ตั้งแต่เริ่ม)
private static ManualResetEventSlim _startGate = new ManualResetEventSlim(false);
public static void RunDemo()
{
Console.WriteLine("[Main] เตรียมปล่อยตัวนักวิ่ง 3 คน...");
// สร้างและเริ่ม Worker Threads 3 ตัว
for (int i = 1; i <= 3; i++)
{
int runnerId = i;
Task.Run(() => Runner(runnerId));
}
// หน่วงเวลาให้ Main Thread เตรียมการ 2 วินาที (ระหว่างนี้นักวิ่งต้องรอที่จุดสตาร์ท)
Thread.Sleep(2000);
Console.WriteLine("[Main] ปัง! ปล่อยตัวได้...");
// เปิดประตูรั้ว! สัญญาณนี้จะปลุกทุก Thread ที่กำลังรอ (Wait) อยู่ให้ตื่นพร้อมกัน
_startGate.Set();
}
private static void Runner(int id)
{
Console.WriteLine($"[Runner {id}] มาถึงจุดสตาร์ท กำลังรอสัญญาณ...");
// สั่งให้ Thread หยุดรอ (Block) จนกว่าจะมีการเรียก Set()
_startGate.Wait();
Console.WriteLine($"[Runner {id}] วิ่งออกตัวแล้ว!");
}
}5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)
ในระดับ Senior Dev นี่คือเกร็ดความรู้และข้อควรระวังเมื่อคุณใช้กลไก Signaling ครับ:
- ระวังสัญญาณสูญหาย (The Lost Signal Race):
ใน
AutoResetEventถ้าคุณเรียกSet()ในขณะที่ “ไม่มี” Thread ไหนกำลังรออยู่ ประตูจะเปิดค้างไว้ (Latch) เพื่อรอ Thread ถัดไปมาถึง แต่ทว่า ถ้าคุณเผลอเรียกSet()รัวๆ 3 ครั้งติดกัน ในตอนที่ยังไม่มีใครมารอ ประตูก็จะจำแค่ว่า “เปิดแล้ว” 1 ครั้งเท่านั้น เมื่อมี Thread 3 ตัววิ่งมาถึง จะมีแค่ 1 ตัวที่ได้ผ่านไป ส่วนอีก 2 ตัวจะติดแหง็ก (Block) ถาวร! - ใช้
WaitHandle.WaitAnyและWaitAll: ออบเจกต์ในตระกูล Wait Handle (รวมถึง Mutex และ Semaphore) สามารถนำมาใช้กับเมธอดสแตติกWaitHandle.WaitAny(รอให้สัญญาณใดสัญญาณหนึ่งมาถึง) หรือWaitAll(รอให้ครบทุกสัญญาณ) ได้ ซึ่งมีประโยชน์มากในการประสานงานหลายๆ Thread เข้าด้วยกัน - Signaling ข้าม Process (Cross-Process):
คุณสามารถส่งสัญญาณข้ามระหว่าง Application 2 ตัวที่รันอยู่คนละหน้าต่างได้! โดยการตั้งชื่อ (Named Event) ให้กับ
EventWaitHandleเช่นnew EventWaitHandle(false, EventResetMode.AutoReset, @"Global\MyCompany.MySignal");แต่จำไว้ว่าฟีเจอร์ตั้งชื่อนี้ใช้กับเวอร์ชัน Slim ไม่ได้ และมีเฉพาะบน Windows เท่านั้นครับ - ประสิทธิภาพ (Performance):
การเรียก
Set()หรือWaitOne()ของรุ่นดั้งเดิมจะใช้เวลาประมาณ 1 ไมโครวินาที (1,000 นาโนวินาที) แต่ถ้าคุณใช้รุ่นManualResetEventSlimมันจะเร็วกว่าถึง 50 เท่า (ราวๆ 20-40 นาโนวินาที) หากเป็นการรอระยะสั้นๆ เพราะมันไม่ต้องวิ่งลงไปเรียก OS ทันทีครับ
6. 🏁 บทสรุป (To be continued…)
Event Wait Handles เป็นเครื่องมือพื้นฐานที่ทรงพลังสำหรับการสื่อสารข้าม Thread หากต้องการปล่อยผ่านทีละคน ให้ใช้ AutoResetEvent แต่หากต้องการปล่อยทีละหลายๆ คน ให้เลือกใช้ ManualResetEventSlim สิ่งสำคัญคือคุณต้องจัดการจังหวะการเรียก Set() และ Wait() ให้ดีเพื่อไม่ให้เกิดสภาวะ Deadlock ครับ
ในตอนหน้า เราจะมาดูอีกหนึ่งเครื่องมือ Signaling ที่ฉลาดและล้ำลึกยิ่งขึ้น ซึ่งออกแบบมาเพื่อใช้รอคอยจนกว่า Thread ย่อยหลายๆ ตัวจะทำงานเสร็จสิ้นทั้งหมดก่อน นั่นก็คือ CountdownEvent และ Barrier เครื่องมือประสานงานขั้นสูงที่จะทำให้การรวมผลลัพธ์เป็นเรื่องง่าย รอติดตามกันได้เลยครับ!
ต้องการที่ปรึกษาด้านการออกแบบสถาปัตยกรรมซอฟต์แวร์และการจัดการระบบ Concurrency ประสิทธิภาพสูงให้กับองค์กรของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและพัฒนาซอฟต์แวร์แบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p