รูปปกบทความ

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 มารอที่ประตู พวกมันจะเข้าคิวรอทีละตัวครับ
  • ManualResetEvent (ประตูรั้วฟาร์มเปิดอ้า - The Corral Gate):
    • มันทำงานเหมือนประตูรั้วฟาร์มม้าครับ (Corral gate) เมื่อประตูถูกล็อก Thread ทุกตัวที่มาถึงจะต้องยืนรอ (เรียก WaitOne())
    • แต่เมื่อไหร่ที่คุณเปิดประตู (เรียกคำสั่ง Set()) Thread ทุกตัวที่รออยู่จะแห่กันวิ่งผ่านประตูไปได้ทั้งหมดพร้อมๆ กัน!
    • ประตูจะเปิดอ้าค้างอยู่อย่างนั้น จนกว่าคุณจะสั่งปิดมันด้วยตัวเอง (เรียกคำสั่ง Reset())
  • การกำเนิดร่าง Slim (ManualResetEventSlim):
    • ในยุค .NET 4.0 Microsoft ได้เปิดตัว ManualResetEventSlim ซึ่งเป็นร่างอัปเกรดที่ใช้เทคนิค Spinning เข้ามาช่วย ทำให้มันเบา (Lightweight) และทำงานเร็วกว่าตัวธรรมดาอย่างมากสำหรับการรอช่วงสั้นๆ โดยไม่ต้องพึ่งพากลไกระดับ OS ทันที
รูปประกอบ Architecture Diagram การทำงานของ AutoResetEvent และ ManualResetEvent

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