Spurious Wakeup — ทำไม condition variable จึงตื่น "โดยไม่ถูกแจ้ง" และวิธีรออย่างถูกต้องบน Windows

· · Windows, มัลติเธรด, Condition Variable, การซิงโครไนซ์, C++, C#, Win32 API, การแก้ปัญหา

“เราใส่ข้อมูลในคิวแล้วปลุกเธรดเวิร์กเกอร์ที่กำลังรอ มันรันมาหกเดือน แล้ววันหนึ่งมันพยายามอ่านคิวว่างแล้วล่ม” “เรากำลังส่งการแจ้ง แต่บางครั้งเธรดไม่เคยตื่น” — การนัดพบแบบมัลติเธรดดูราวกับว่าทำงาน และเป็นแหล่งเพาะบั๊กที่ปรากฏเฉพาะน้อยครั้ง การสอบสวนชนิดนี้มักลงที่โค้ดที่ห่อ wait ของ condition variable ด้วย if และข้างหลังนั้นนั่ง spurious wakeup — ปรากฏการณ์ของการกลับจาก wait โดยไม่ได้รับการแจ้ง

“มันตื่นแม้ไม่มีใครแจ้ง” ฟังเหมือนข้อบกพร่องของการอิมพลีเมนต์ แต่เป็นพฤติกรรมที่ Win32, C++ และ POSIX ทั้งหมดระบุในเอกสารหรือมาตรฐาน และ Monitor ของ .NET ถูกออกแบบบนสมมติฐานว่า “เมื่อถูกปลุก คุณตรวจเงื่อนไขอีกครั้ง” ทำไมพฤติกรรมนั้นจึงถูกอนุญาต มันเกิดที่ชั้นใดบน Windows และคุณเขียนการรออย่างไรให้ไม่ชน มัน มุ่งไปที่นักพัฒนาที่เขียนแอปธุรกิจและซอฟต์แวร์ควบคุมอุปกรณ์บน Windows บทความนี้แกะว่า spurious wakeup คืออะไรจริง ๆ จากแหล่งปฐมภูมิ และต้มการรอที่ถูกต้องลงเป็น Win32 (C), C++ และ C#

1. สรุปก่อนเลย

  • wait ของ condition variable สามารถกลับได้แม้ไม่มีการแจ้งมาถึง เอกสารทางการของ Win32 ระบุว่า condition variable อยู่ภายใต้ spurious wakeup (การตื่นที่ไม่ผูกกับการปลุกที่ชัดเจน) และ stolen wakeup (เธรดอื่นบริโภคเงื่อนไขก่อนเธรดที่ถูกปลุก)1
  • ดังนั้นคุณต้องเขียนการรอเสมอเป็น “ลูป while บวกการตรวจเงื่อนไขอีกครั้ง” โค้ดที่ตรวจครั้งเดียวด้วย if แล้ว wait ดูราวกับว่าทำงาน และซ่อนบั๊กที่ทำซ้ำได้เฉพาะน้อยครั้ง12
  • นี่ไม่ใช่ความแปลกเฉพาะ Windows POSIX และมาตรฐาน C++ กล่าวสิ่งเดียวกัน การอิมพลีเมนต์ที่ “ไม่เคยตื่นปลอมเด็ดขาด” จะทำให้การทำงานของ condition variable ทุกครั้งช้าลง ดังนั้นการตื่นถูกอนุญาตบนสมมติฐานว่าฝั่งที่รอจะตรวจอีกครั้ง34
  • ใน C++ รูป predicate wait(lock, pred) ให้ไลบรารีทำลูปแทนคุณ รูปนั้นโดยพฤตินัยรัน while (!pred()) wait(lock); มันคือค่าเริ่มต้นสำหรับโค้ดใหม่5
  • Monitor.Wait ของ C# ต้องการวินัยเดียวกัน เงื่อนไขอาจถูกบริโภคในช่วงระหว่างถูกปลุกกับการถือล็อกใหม่ ดังนั้นคุณตรวจเงื่อนไขอีกครั้งใน while แล้วกลับไป Wait6
  • อัปเดตและตรวจเงื่อนไขภายใต้ล็อกเดียวกัน หากคุณดูเงื่อนไขนอกล็อกแล้วเข้า wait การแจ้งสามารถผ่านช่องว่าง — lost wakeup1
  • อย่าสร้างการแจ้งชั่วคราวแบบ “ปลุกผู้ที่กำลังรอตอนนี้” ของ condition variable ด้วย pulse บนอีเวนต์ PulseEvent โดยเฉพาะอาจพลาดการแจ้งในชั่วขณะที่ kernel-mode APC ยกการรอสั้น ๆ และ Microsoft เองกล่าวตรง ๆ ว่า “มันไม่น่าเชื่อถือ อย่าใช้ ใช้ condition variable แทน”7

สิ่งที่ตามมาเดินผ่านกลไกที่รองรับข้อสรุปนี้ ตามลำดับ

2. Spurious Wakeup คืออะไร — การตื่นไม่หมายความว่าเงื่อนไขเป็นจริง

condition variable คือพริมิทีฟซิงโครไนซ์สำหรับ “ให้เธรดหลับจนกว่าเงื่อนไขบางอย่างเป็นจริง และให้มันถูกปลุกเมื่อเป็นจริง” บน Win32 นั่นคือโครงสร้าง CONDITION_VARIABLE คู่กับ SleepConditionVariableCS / SleepConditionVariableSRW (รอ) และ WakeConditionVariable / WakeAllConditionVariable (แจ้ง) API รอปล่อยล็อกที่คุณถืออยู่อย่างอะตอมมิก (critical section หรือ SRW lock) แล้วไปหลับ และตอนตื่นมันถือล็อกใหม่ก่อนกลับ1

คำถามคือความจริงของ “ได้กลับจาก wait” หมายความว่าอะไรจริง ๆ โดยไร้เดียงสาคุณอยากคิดว่า “การแจ้งมาถึง = เงื่อนไขเป็นจริง” แต่ในความเป็นจริงมีสามกรณีที่ wait กลับ

กรณี การแจ้ง เงื่อนไขตอนกลับ
การตื่นจริง มี มักเป็นจริง แต่ไม่ถูกประกัน
Spurious wakeup ไม่มีที่ส่งถึงคุณ ยังไม่เป็นจริง
Stolen wakeup มี เธรดอื่นบริโภคก่อน ไม่เป็นจริง
สามกรณีที่ wait กลับการรอของ condition variable สามารถกลับได้ไม่เฉพาะจากการแจ้งจริง แต่ยังจาก spurious wakeup ที่ไม่มีการแจ้ง และจาก stolen wakeup ที่การแจ้งมาถึงแต่เงื่อนไขถูกบริโภคก่อน ดังนั้นทุกกรณีต้องตรวจเงื่อนไขอีกครั้งกลับจาก waitการแจ้งจริงSpurious wakeup (ไม่มีการแจ้ง)Stolen wakeup (เงื่อนไขถูกบริโภคแล้ว)ตรวจเงื่อนไขอีกครั้ง แล้วเดินต่อ

ภาพ 1: มีสามเส้นทางกลับจาก wait และผู้เรียกบอกไม่ได้ว่าเดินเส้นใด ดังนั้นคุณต้องตรวจเงื่อนไขอีกครั้งเสมอ

spurious wakeup คือกรณีที่สองนี้ — ปรากฏการณ์ที่ API รอกลับโดยไม่ผูกกับการแจ้งที่ชัดเจนที่ตั้งใจปลุกคุณ มันไม่จำกัดเฉพาะสถานการณ์ที่ WakeConditionVariable ไม่เคยถูกเรียกที่ใดในระบบ ตัวอย่างเช่น ภายใต้โหลดสูงที่การแจ้งมาถึงเป็นชุดสั้น ๆ การอิมพลีเมนต์อาจปลุกเธรดที่รอเพิ่มเป็นชุด และจากฝั่งที่ไม่มีแจ้งที่สอดคล้อง นั่นก็คือ spurious wakeup เช่นกัน หน้า condition variable ของ Microsoft Learn กล่าวสิ่งนี้ตรง ๆ: “Condition variables are subject to spurious wakeups (those not associated with an explicit wake) and stolen wakeups (another thread manages to run before the woken thread). Therefore, you should recheck a predicate (typically in a while loop) after a wait operation returns.”1

จุดสำคัญคือผู้เรียกบอกไม่ได้ว่ากลับผ่านกรณีใดในสามกรณี หากบอกไม่ได้ มีกลยุทธ์เดียวที่ใช้ได้: ทุกครั้งที่กลับ ให้ตรวจเงื่อนไขที่คุณกำลังรอเอง และกลับไปหลับหากมันไม่เป็นจริง นั่นคือเนื้อหาจริงของกฎเหล็ก “ห่อ wait ด้วย while” พูดกลับกัน ตราบที่คุณคงกฎนั้น โค้ดถูกต้องไม่ว่ากรณีใดในสามกรณีปลุกคุณ

3. ทำไมข้อกำหนดจึงอนุญาต — การแจ้งที่แม่นยำแพง

“การตื่นโดยไม่ถูกแจ้งเป็นการอิมพลีเมนต์ที่เลอะเทอะไม่ใช่หรือ” เป็นคำถามที่ยุติธรรม จริง ๆ แล้วเป็นไปได้ในทางทฤษฎีที่จะสร้างการอิมพลีเมนต์ที่ไม่เคยตื่นปลอม แม้เช่นนั้น POSIX, Windows และมาตรฐาน C++ ทั้งหมดลงเอยฝั่ง “มันเกิดได้” เหตุผลถูกกล่าวตรง ๆ ใน Rationale ของ pthread_cond_wait ใน POSIX (The Open Group Base Specifications)3

เหตุผลแรกคือประสิทธิภาพ การพยายามอิมพลีเมนต์การแจ้งที่ “ปลุกเธรดพอดีหนึ่งเส้นอย่างน่าเชื่อถือ” อย่างเคร่งครัด โดยเฉพาะบนมัลติโปรเซสเซอร์ เพิ่มต้นทุนซิงโครไนซ์พิเศษให้การทำงานของ condition variable ทุกครั้ง ตัวจัดตารางนั่งระหว่างการแจ้งกับการปลุก และขึ้นกับจังหวะของการขัดจังหวะและการแย่ง คุณเลี่ยง “เธรดอื่นรันก่อนเธรดที่คุณตั้งใจปลุก” ไม่ได้ การให้ทุกคนจ่ายต้นทุนของการปิดสิ่งนั้นให้สนิทแย่กว่า สำหรับการทำให้ condition variable เร็ว กว่าการยอมรับว่า “คุณอาจปลุกเพิ่มเป็นครั้งคราว”

เหตุผลที่สองคือข้อสังเกตว่าการแลกนี้ไม่ทำลายแอป — มันทำให้พวกมันแข็งแรงกว่าจริง ๆ เพราะ spurious wakeup ถูกอนุญาต โค้ดที่ถูกต้องเขียนลูปที่ตรวจ predicate (เงื่อนไขที่กำลังรอ) เสมอ Rationale ของ POSIX กล่าวว่าการบังคับลูปนี้ทำให้โค้ดอธิบายตนเองและแข็งแรงกว่า3 เมื่อลูปอยู่แล้ว ความหมายของการแจ้งถูกลดจาก “ประกันว่าเงื่อนไขเป็นจริง” เป็น “คำใบ้ว่าเงื่อนไขอาจเปลี่ยน” และฝั่งที่รอทนต่อการเปลี่ยนการออกแบบเล็กน้อยของฝั่งที่แจ้ง (ปลุกมากเกินไป ปลุกเป็นชุด เป็นต้น)

stolen wakeup เป็นเรื่องเชิงโครงสร้างยิ่งกว่า มีช่องว่างของเวลาเสมอระหว่างที่ฝั่งที่แจ้งเรียก WakeConditionVariable กับที่เธรดที่ถูกปลุกถือล็อกใหม่แล้วกลับจาก wait หากเธรดที่สามถือล็อกในช่วงนั้นได้ มันสามารถบริโภคเงื่อนไข (เนื้อหาของคิว เป็นต้น) ก่อน นั่นคือช่องว่างที่การขัดเกลาการอิมพลีเมนต์ไม่ว่าเท่าใดก็ลบไม่ได้ เพราะมันมาจากรูปของเครื่องมือ condition variable เอง

ไทม์ไลน์ของ stolen wakeupผู้ผลิตใส่หนึ่งรายการในคิวแล้วปลุกผู้บริโภค A ที่กำลังรอ แต่ก่อน A ถือล็อกใหม่ ผู้บริโภค B ถือล็อกแล้วเอาหนึ่งรายการ ดังนั้นคิวว่างเมื่อ A ตื่นผู้บริโภค Bผู้ผลิตผู้บริโภค A (กำลังรอ)ผู้บริโภค Bผู้ผลิตผู้บริโภค A (กำลังรอ)ถูกปลุก กำลังรอถือล็อกใหม่คิวว่าง (ถูกขโมย)เพิ่มหนึ่งรายการเข้าคิวWakeConditionVariableถือล็อกแล้วเอาหนึ่งรายการถือล็อกใหม่แล้วกลับจาก waitตรวจอีกครั้งในลูป while แล้วรออีก

ภาพ 2: “stolen wakeup” ที่เธรดที่สามบริโภคเงื่อนไขในช่องว่างเวลาระหว่างการแจ้งกับการปลุก สามารถเกิดภายใต้การอิมพลีเมนต์ใดก็ได้

กล่าวอีกนัย แม้ OS กำจัด spurious wakeup ให้หมดสิ้น ตราบที่ stolen wakeup มีอยู่ คุณยังเขียน “ฉันตื่น = เงื่อนไขเป็นจริง” ไม่ได้ ลูปตรวจอีกครั้งของฝั่งที่รอถูกต้องการอยู่แล้ว และเมื่อเป็นเช่นนั้น การอนุญาต spurious wakeup แล้วคงการอิมพลีเมนต์ให้เร็วถูกกว่า — นั่นคือการตัดสินออกแบบที่ condition variable ถือมานานหลายทศวรรษ

4. มันปรากฏที่ชั้นใดบน Windows

คุณสมบัตินี้โชว์หน้าไม่ว่าคุณจะใช้ชั้นใดของพริมิทีฟซิงโครไนซ์บน Windows เพื่อรู้สึกว่าคุณหนีมันไม่ได้ไม่ว่าจะเขียนกับ API ของชั้นใด เราจะดูชั้นตัวแทน

condition variable ของ Win32 (CONDITION_VARIABLE) ตามที่กล่าวแล้ว ถูกบันทึกบน SleepConditionVariableCS / SleepConditionVariableSRW ว่าอยู่ภายใต้ทั้ง spurious wakeup และ stolen wakeup และคุณถูกต้องการให้ตรวจ predicate อีกครั้งในลูป while2 ตัวอย่างการใช้งานอย่างเป็นทางการ (คิวผู้ผลิต–ผู้บริโภค) ก็เขียนการรอภายในลูป while8

WaitOnAddress ที่ชั้นต่ำกว่าอีก คือ API รอที่ดั้งเดิมกว่า condition variable: “รอจนกว่าค่าที่ที่อยู่ที่ให้เปลี่ยน” (Windows 8 เป็นต้นไป) แม้ API ใกล้ก้นชั้นนี้ก็มีเอกสารที่ระบุว่า “มันถูกประกันว่าจะกลับเมื่อที่อยู่ถูกสัญญาณ แต่ยังได้รับอนุญาตให้กลับด้วยเหตุอื่น” และยกตัวอย่างของการตื่นเร็วเป็นสภาพหน่วยความจำต่ำ การทิ้งการปลุกก่อนหน้าสำหรับที่อยู่เดียวกัน และการรัน checked build นั่นคือเหตุที่ตัวอย่างการใช้งานของเอกสารเองอยู่ในรูปของ “ลูป while ที่เปรียบเทียบค่าอีกครั้ง”9

std::condition_variable ของ C++ ก็เหมือนกัน เอกสารของ MSVC กล่าวถึง wait ที่ไม่มี predicate ว่ามัน “บล็อกจนกว่าถูกสัญญาณโดยการเรียก notify_one / notify_all มันยังอาจตื่นปลอม” และอธิบายว่ารูป predicate wait(lock, pred) โดยพฤตินัยรันโค้ดต่อไปนี้5

while (!Pred())
    wait(Lck);

กล่าวอีกนัย wait รูป predicate ที่แนะนำใน C++ ไม่ใช่อื่นใดนอกจากไลบรารีรับ “ห่อด้วย while” ตามที่บทความนี้อธิบาย แทนคุณ cppreference เช่นกันระบุว่า wait ที่ไม่มี predicate สามารถถูกปลดบล็อกแบบปลอมได้4

Monitor.Wait / Pulse ของ .NET มีโครงสร้างคิวของตนเองคือคิวรอและคิวพร้อม แต่วินัยไม่เปลี่ยน เธรดที่ถูกปลุกโดย Pulse / PulseAll ย้ายไปคิวพร้อมแล้วกลับจาก Wait ตามลำดับที่ถือล็อกใหม่ได้ การที่เธรดอื่นบริโภคเงื่อนไขได้ในช่วงก่อนล็อกถูกถือใหม่เหมือนกับบน Win32 และเอกสารก็ถูกเขียนบนสมมติฐานว่า “เธรดที่ถูกปลุกประเมินเงื่อนไขที่ทำให้มันเข้าสู่การรออีกครั้ง และเรียก Wait อีกหากจำเป็น”610

ทุกชั้นต้องการให้ตรวจ predicate อีกครั้งเอกสารทางการต้องการให้ตรวจเงื่อนไขอีกครั้งหลังตื่นที่ทุกชั้น — C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE และ WaitOnAddress ชั้นต่ำC++ std::condition_variableตอนตื่น ตรวจเงื่อนไขอีกครั้ง (while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

ภาพ 3: เปลี่ยนภาษาหรือเฟรมเวิร์ก และข้อกำหนดทางการยังเหมือนกันที่ทุกชั้นของพริมิทีฟรอ: ตรวจอีกครั้งหลังคุณตื่น

5. วิธีรอที่ถูกต้อง — เขียนด้วย while และ predicate

จากนี้ไป การอิมพลีเมนต์ มีหลักการเพียงสามข้อ

  1. ถือสิ่งที่คุณรอเป็นสถานะ (predicate) ไม่ใช่เป็น “การแจ้ง” เงื่อนไขคือสถานะที่ใช้ร่วมที่ถูกล็อกปกป้อง — “คิวไม่ว่างหรือไม่” “แฟล็กถูกตั้งหรือไม่” — ไม่ใช่ “ฉันถูกปลุกหรือไม่”
  2. วาง wait ภายในลูป while บนเงื่อนไขเสมอ แต่ละครั้งที่ตื่น ตรวจเงื่อนไข และกลับไปหลับหากไม่เป็นจริง
  3. อัปเดตและตรวจเงื่อนไขภายใต้ล็อกเดียวกัน ฝั่งที่แจ้งอัปเดตสถานะแล้วจึงแจ้ง
ลำดับของลูปรอที่ถูกต้องถือล็อกแล้วตรวจเงื่อนไข หากไม่เป็นจริง ปล่อยล็อกแล้วหลับ ตอนตื่นถือล็อกใหม่แล้วกลับสู่การตรวจเงื่อนไข เดินต่อโดยถือล็อกเฉพาะเมื่อเงื่อนไขเป็นจริงไม่ใช่ถือล็อกเงื่อนไขเป็นจริงหรือไม่?wait (ปล่อยล็อกแล้วหลับ)ตื่น (ถือล็อกใหม่)เดินต่อขณะยังถือล็อก

ภาพ 4: การรอที่ถูกต้องคือลูป และไม่มีช่องว่างระหว่างการตรวจเงื่อนไขกับการประมวลผล (ทั้งคู่เกิดขณะถือล็อก)

รูปนี้มีประโยชน์ที่พลาดง่าย ชั่วขณะที่คุณออกจากลูป while มันถูกสถาปนา ขณะที่คุณยังถือล็อก ว่า “เงื่อนไขเป็นจริง” ลูปที่ป้องกัน spurious wakeup คือ ตามที่เป็น ประกันว่าไม่มีช่องว่างเงื่อนไขแข่งระหว่างการตรวจเงื่อนไขกับการประมวลผลมัน

รูปพื้นฐานใน Win32 (C)

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // shared state protected by cs

// Initialise once at startup (for static initialisation, cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);

// Waiter (consumer)
EnterCriticalSection(&cs);
while (queueCount == 0) {                       // always while, never if
    SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// Here the lock is held and queueCount > 0 is guaranteed
--queueCount;
LeaveCriticalSection(&cs);

// Notifier (producer)
EnterCriticalSection(&cs);
++queueCount;                                    // update the state under the lock
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // notifying after releasing the lock is fine

คุณเรียกการแจ้ง (WakeConditionVariable) จากภายในล็อกหรือจากภายนอกได้ แต่เอกสารกล่าวว่าการปลุกหลังปล่อยล็อกมักดีกว่า เพื่อลดการสลับบริบท1 ในทางกลับกัน การอัปเดตสถานะเอง (++queueCount) ต้องเกิดภายใต้ล็อกเสมอ อย่าสับสนทั้งสองอย่าง

รูปพื้นฐานใน C++ — ทำให้ predicate wait เป็นค่าเริ่มต้น

std::mutex m;
std::condition_variable cv;
std::queue<Item> q;

// Waiter
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); });   // internally while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();

// Notifier
{
    std::lock_guard<std::mutex> lk(m);
    q.push(std::move(item));
}
cv.notify_one();

เพราะ wait รูป predicate ทำลูปแทนคุณ while ที่เขียนมือจึงไม่จำเป็น เมื่อคุณแก้โค้ดที่มีอยู่ที่ยังมีลูปเขียนมือ while (q.empty()) cv.wait(lk); คือรูปที่ถูกต้อง จึงไม่จำเป็นต้องรีบเขียนใหม่ รูปเดียวที่ไม่ถูกต้องคือ if (q.empty()) cv.wait(lk);

รูปพื้นฐานใน C#

private readonly object _gate = new();
private readonly Queue<Item> _queue = new();

// Waiter
lock (_gate)
{
    while (_queue.Count == 0)          // always while, never if
    {
        Monitor.Wait(_gate);
    }
    var item = _queue.Dequeue();
}

// Notifier
lock (_gate)
{
    _queue.Enqueue(item);
    Monitor.Pulse(_gate);              // Monitor.Pulse can only be called inside the lock
}

Monitor.Wait / Pulse / PulseAll เรียกได้เฉพาะจากภายในล็อก (บล็อก lock) ซึ่งต่างจาก Win32 การเรียกนอกล็อกโยน SynchronizationLockException10

การรอพร้อมหมดเวลา — คำนวณเวลาที่เหลือจากเส้นตาย

เมื่อคุณรอพร้อมหมดเวลา การส่ง “ค่าหมดเวลาเดียวกัน” ทุกครั้งที่วนลูปยืดการรอแต่ละครั้งที่ spurious wakeup เกิด รูปที่ถูกต้องคือตรึงเส้นตายก่อนแล้วคำนวณเวลาที่เหลือใหม่

ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
    ULONGLONG now = GetTickCount64();
    if (now >= deadline) {
        break;                          // timeout (condition still unsatisfied)
    }
    if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
        GetLastError() != ERROR_TIMEOUT) {
        break;                          // on failure other than timeout, stop waiting and leave
    }
    // Confirm ERROR_TIMEOUT finally via the while condition and the deadline check
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
ลำดับที่ถูกต้องของการรอพร้อมหมดเวลาตรึงเส้นตายก่อน และแต่ละครั้งที่ตื่นตรวจเงื่อนไขกับเส้นตาย หากยังมีเวลา คำนวณเวลาที่เหลือใหม่แล้วกลับสู่การรอใช่ไม่ใช่ไม่ตรึงเส้นตายเงื่อนไขเป็นจริงหรือไม่?เดินต่อไปประมวลผลเส้นตายผ่านไปแล้วหรือไม่?จัดการหมดเวลาคำนวณเวลาที่เหลือแล้วรอ

ภาพ 5: การรอพร้อมหมดเวลาไม่ส่ง “ระยะรอเดียวกัน” อีก มันคำนวณเวลาที่เหลือใหม่จากเส้นตาย

ใน C++ คุณปล่อยการคำนวณนี้ รวมเส้นตาย ให้โอเวอร์โหลด wait_until (เวลาสัมบูรณ์) บวก predicate ได้ แม้เมื่อมันกลับตอนหมดเวลา มันให้ค่าสุดท้ายของ predicate ดังนั้นคุณยังตัดสิน “เราหมดเวลา หรือเราทัน” จาก predicate ได้5

6. รายการรูปแบบที่ควรหลีกเลี่ยง

การตรวจครั้งเดียวด้วย if นี่คือดาวของบทความ ชั่วขณะที่ spurious wakeup หรือ stolen wakeup เกิด การประมวลผลเดินต่อโดยเงื่อนไขไม่เป็นจริง การเอาจากคิวว่าง การแตะข้อมูลที่ยังไม่เริ่มต้น double free — อาการกลายเป็น “การล่มหรือการเสียข้อมูลที่ปรากฏเฉพาะเป็นครั้งคราว”

การตรวจหรืออัปเดตเงื่อนไขนอกล็อก หากฝั่งที่รอดูเงื่อนไขนอกล็อก ตัดสินว่า “ยังไม่” และในช่องว่างก่อนเข้า wait ฝั่งที่แจ้งอัปเดตสถานะแล้วส่งการแจ้ง การแจ้งถูกยิงไปที่ condition variable ที่ไม่มีผู้รอแล้วหายไป ฝั่งที่รอจากนั้นเข้า wait แล้วรอการแจ้งที่จะไม่มาอีก นั่นคือ lost wakeup ภาพสะท้อนของ spurious wakeup เหตุที่ API รอของ condition variable ถูกออกแบบให้ “ปล่อยล็อกอย่างอะตอมมิกแล้วไปหลับ” คือเพื่อปิดช่องว่างนี้พอดี1 มันไม่เกิดตราบที่คุณคงวินัยล็อก

ไทม์ไลน์ของ lost wakeupหากฝั่งที่รอตรวจเงื่อนไขนอกล็อก และฝั่งที่แจ้งอัปเดตสถานะแล้วแจ้งในช่องว่างก่อนเข้า wait การแจ้งถูกส่งไปยัง condition variable ที่ไม่มีผู้รอแล้วหายไป และฝั่งที่รอรอการแจ้งที่จะไม่มาอีกฝั่งที่แจ้งฝั่งที่รอฝั่งที่แจ้งฝั่งที่รอไม่มีผู้รอชั่วขณะนี้การแจ้งหายไปแล้วและมันไม่เคยตื่นตรวจเงื่อนไขนอกล็อก (ไม่เป็นจริง)อัปเดตสถานะแล้วแจ้งเข้า wait

ภาพ 6: หากคุณตรวจเงื่อนไขนอกล็อก การแจ้งลอดผ่านช่องว่างระหว่างการตรวจกับ wait — “lost wakeup”

การสร้าง “การแจ้งชั่วคราว” ของ condition variable ด้วย pulse บนอีเวนต์ อีเวนต์เอง (CreateEvent + SetEvent) ไม่ใช่แอนตี้แพตเทิร์น สัญญาณปลุกในการตั้งค่าที่ผู้บริโภคคนเดียวประมวลผลคิวจนว่าง หรือคำสั่งหยุดที่เมื่อยกแล้วไม่ถูกหย่อน (manual-reset event) คือการใช้ที่ถูกต้องของอีเวนต์ และเมื่อคุณอยากรวมกับเป้าหมายรออื่นผ่าน WaitForMultipleObjects หรือข้ามขอบโปรเซส condition variable — ออบเจ็กต์โหมดผู้ใช้ที่แชร์ข้ามโปรเซสไม่ได้ — คืออันที่ใช้ไม่ได้1 สิ่งที่อันตรายคือการพยายามสร้าง ด้วยการทำงานของอีเวนต์ การแจ้งชั่วคราวของ condition variable ที่ “ปลุกเฉพาะเธรดที่กำลังรอชั่วขณะนั้นและไม่ทิ้งสถานะ” ความคิดนั้นเกือบเสมอพาไปยังข้อถัดไป PulseEvent

การใช้ PulseEvent มันคือ API ที่บน manual-reset event “ปลุกทุกคนที่กำลังรอแล้วคืนอีเวนต์สู่สถานะไม่สัญญาณทันที” แต่ Microsoft เองระบุในเอกสารว่า “ฟังก์ชันนี้ไม่น่าเชื่อถือและไม่ควรใช้ มันมีอยู่หลัก ๆ เพื่อความเข้ากันได้ย้อนหลัง ใช้ condition variable แทน” เหตุผลคือเธรดที่กำลังรอสามารถถูกถอดออกจากสถานะรอชั่วคราวโดย kernel-mode APC แล้วกลับสู่การรอหลัง APC เสร็จ หาก PulseEvent ถูกเรียกในช่วงสั้นนั้น เธรดนั้นไม่ถูกรวมใน “ผู้ที่กำลังรอชั่วขณะที่ถูกเรียก” และไม่ถูกปลุก7 kernel APC คือสิ่งที่ OS ใช้ภายใน แอปควบคุมไม่ได้11 ปัญหานี้ยังเป็นคำเตือนการวิเคราะห์สแตติก (C28648)12 หาก spurious wakeup คือปัญหาของ “ปลุกเพิ่ม” นี่คือปัญหาของ “นอนเกินเมื่อควรตื่น” และลูป while ช่วยไม่ได้ — เพราะการแจ้งเองหายไป

การส่งเฉพาะการแจ้งก่อน โดยไม่ถือล็อก ก่อนอัปเดตสถานะ การเรียก WakeConditionVariable ขณะสถานะยังเก่า แล้วจึงถือล็อกและอัปเดตสถานะ — ในลำดับนั้น เธรดที่ถูกปลุกยังเห็นเงื่อนไขไม่เป็นจริงเมื่อตรวจ แล้วกลับไปหลับ หากไม่มีการแจ้งเพิ่ม มันอยู่ที่นั่น โปรดทราบว่าหากคุณเขียน “แจ้ง → อัปเดต → ปล่อย” ขณะยังถือล็อกเดียวกัน ไม่มีอันตรายจริง เพราะฝั่งที่รอตรวจเงื่อนไขไม่ได้จนกว่าถือล็อกใหม่ แม้เช่นนั้น เพื่อไม่ให้ผู้อ่านต้องตรวจเงื่อนไขความปลอดภัยนี้ทุกครั้ง จะปลอดภัยกว่าหากทำให้ลำดับ “อัปเดตสถานะภายใต้ล็อก แล้วแจ้งหลังจากนั้น” เป็นมาตรฐาน

7. วิธีสอบสวนเมื่อคุณพบมัน

บั๊กที่เกี่ยวข้องกับ spurious wakeup มีลักษณะ “ปรากฏเฉพาะน้อยครั้ง” ทำงานย้อนจากอาการ พวกมันแยกเป็นสองตระกูลต่อไปนี้

ตระกูล 1: การประมวลผลเดินต่อโดยเงื่อนไขไม่เป็นจริง ข้อยกเว้นหรือการล่มจากการเอาจากคิวว่าง ผลที่ขาด เป็นต้น สงสัยการรอที่ไม่มี predicate คุณหวีสิ่งนี้ออกได้เชิงกลในการตรวจทานโค้ด — ค้นหาที่ที่ cv.wait( มีอาร์กิวเมนต์เดียว และที่ที่ SleepConditionVariableCS / Monitor.Wait ถูกห่อด้วย if แทน while การตรวจนี้ไม่ต้องการรอการทำซ้ำ และเป็นการเคลื่อนที่ที่มีเลเวอเรจสูงสุดที่คุณมี

ตระกูล 2: เธรดที่ควรตื่นไม่ตื่น (แฮง) สงสัย lost wakeup (การตรวจเงื่อนไขนอกล็อก หรือการแจ้งนอกล็อกก่อนอัปเดตสถานะ) และ PulseEvent ถ่ายดัมป์จากโปรเซสที่แฮงแล้วดูสแตกของแต่ละเธรด และคุณระบุได้ว่าเธรดใดติดใน API รอใด จากที่นั่น ไล่ผ่านโค้ด “ใครควรส่งการแจ้งนั้น และในลำดับใด”

ลำดับการแยกจากอาการหากการประมวลผลเดินต่อโดยเงื่อนไขไม่เป็นจริง ให้หวีการรอที่ไม่มี predicate ด้วยการค้นหาโค้ด หากเธรดไม่ตื่น ให้ระบุจุดรอจากดัมป์แล้วสงสัย lost wakeup หรือ PulseEventบั๊กที่ปรากฏเฉพาะน้อยครั้งการประมวลผลเดินต่อโดยเงื่อนไขไม่เป็นจริงเธรดที่ควรตื่นไม่ตื่นค้นหาโค้ดสำหรับการรอที่ไม่มี predicateระบุเธรดที่กำลังรอจากดัมป์เปลี่ยน if เป็น while หรือใช้ predicate waitสงสัย lost wakeup หรือ PulseEvent

ภาพ 7: ว่าอาการคือ “เดินไกลเกินไป” หรือ “ไม่เคยตื่น” แยกทั้งสิ่งที่คุณสงสัยและวิธีสอบสวน

หากคุณอยากทำซ้ำ การเคลื่อนที่มาตรฐานคือขยายหน้าต่างแข่ง เพิ่มความสั่นของจังหวะด้วยการใช้เธรดมากกว่าคอร์กายภาพ การแทรก Sleep โดยเจตนาระหว่าง wait กับ notify และการรันทั้งบิลด์ดีบักและรีลีส เมื่อคุณยืนยันว่า “มันหยุดทำซ้ำหลังเราแก้การรอที่ไม่มี predicate” ให้เปรียบเทียบภายใต้ความเครียดเดียวกัน

8. สรุป — รายการตรวจ

  • เส้นทางกลับจาก wait มีสาม — การแจ้งจริง spurious wakeup และ stolen wakeup — และผู้เรียกแยกพวกมันไม่ได้ ดังนั้นเขียนการรอเป็นลูป while บนเงื่อนไขเสมอ
  • spurious wakeup คือพฤติกรรมที่ Win32, C++ และ POSIX อนุญาตโดยเจตนาเป็นการแลกกับประสิทธิภาพ และมันจะไม่หายไปด้วยการแก้ของ OS หรือการสลับไลบรารี Monitor.Wait ของ .NET ไม่ถูกสมมติว่าตื่นโดยไร้เหตุ แต่เพราะ stolen wakeup และหมดเวลา มีอยู่ วินัย while เดียวกันยังถูกต้องการ
  • ใน C++ ใช้รูป predicate wait(lock, pred) เป็นค่าเริ่มต้น ไลบรารีทำลูป
  • อัปเดตและตรวจเงื่อนไขภายใต้ล็อกเดียวกัน ส่งการแจ้ง “หลังอัปเดตสถานะ” การแจ้งของ Win32/C++ เกิดหลังปล่อยล็อกได้ Pulse ของ C# อยู่ภายในล็อกเท่านั้น
  • สำหรับการรอพร้อมหมดเวลา ตรึงเส้นตายแล้วคำนวณเวลาที่เหลือใหม่ ใน C++ คือ wait_until บวก predicate
  • อย่าสร้างการแจ้งชั่วคราวของ condition variable ด้วย pulse บนอีเวนต์ PulseEvent โดยเฉพาะคือสิ่งที่เอกสารทางการระบุตรง ๆ ว่า “อย่าใช้ ใช้ condition variable แทน” อีเวนต์เองยังเป็นเครื่องมือที่ถูกสำหรับคำสั่งหยุด การรวมกับ WaitForMultipleObjects และการซิงโครไนซ์ข้ามโปรเซส
  • ในการตรวจทาน ค้นหาเชิงกลสำหรับ “การรอที่ไม่มี predicate” และ “if + wait” คุณฆ่าบั๊กที่ทำซ้ำน้อยครั้งได้โดยไม่รอการทำซ้ำ

spurious wakeup ตรงข้ามกับความแปลกของชื่อ รวมเหลือคำสำคัญหนึ่งบรรทัดสำหรับการแก้ — เปลี่ยน if เป็น while และข้างหลังบรรทัดนั้นนอนแนวคิดออกแบบของเครื่องมือ condition variable: “การแจ้งที่แม่นยำแพง ดังนั้นการตรวจเป็นความรับผิดชอบของฝั่งที่รอ” เข้าใจมันเป็นกลไก แล้วคุณควรใช้วินัยเดียวกันโดยไม่ลังเลเมื่อภาษาหรือเฟรมเวิร์กเปลี่ยน

บทความที่เกี่ยวข้อง

ด้านให้คำปรึกษาที่เกี่ยวข้อง

KomuraSoft LLC รับการตรวจทานการออกแบบมัลติเธรด การสอบสวนหาสาเหตุ (การวิเคราะห์ดัมป์) ของการล่มและแฮงที่ “ทำซ้ำได้เฉพาะเป็นครั้งคราว” และการย้ายโค้ดซิงโครไนซ์เดิม (ที่พึ่งอีเวนต์และ PulseEvent เป็นต้น) ไปสู่ฐาน condition variable เริ่มจากการแยกอาการก็ได้ — โปรดติดต่อได้ตามสบาย

ลิงก์อ้างอิง

  1. Microsoft Learn, Condition Variables. ว่าด้วย condition variable เป็นออบเจ็กต์โหมดผู้ใช้ที่ปล่อยล็อกอย่างอะตอมมิกแล้วเข้าสู่การรอ ว่าด้วยมี spurious wakeup (การตื่นที่ไม่ผูกกับการปลุกที่ชัดเจน) และ stolen wakeup (เธรดอื่นรันก่อนเธรดที่ถูกปลุก) จึงหลังกลับจาก wait คุณควรตรวจ predicate อีกครั้งในลูป while และว่าด้วยการแจ้งเป็นไปได้จากภายในหรือภายนอกล็อก แต่การปลุกหลังปล่อยล็อกดีกว่าสำหรับการลดการสลับบริบท  2 3 4 5 6 7 8

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). ว่าด้วยการปล่อย critical section ที่ระบุอย่างอะตอมมิกแล้วรอบน condition variable ว่าด้วยเธรดที่ถูกปลุกถือ critical section ใหม่ก่อนกลับ ว่าด้วย ERROR_TIMEOUT ถูกคืนตอนหมดเวลา และว่าด้วยมี spurious wakeup และ stolen wakeup จึงหลังกลับจาก wait คุณควรตรวจ predicate อีกครั้ง (โดยทั่วไปในลูป while)  2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. ว่าด้วย spurious wakeup จาก pthread_cond_wait / pthread_cond_timedwait สามารถเกิดได้ ว่าด้วยการกลับจาก wait ไม่ได้หมายความอะไรเกี่ยวกับค่าของ predicate จึงควรประเมิน predicate อีกครั้ง และว่าด้วย Rationale ระบุว่าการอิมพลีเมนต์ที่ “ปลุกพอดีหนึ่ง” สามารถทำให้การทำงานของ condition variable ช้าลงโดยเฉพาะบนมัลติโปรเซสเซอร์ และการอนุญาต spurious wakeup บังคับลูปตรวจ predicate แล้วทำให้แอปแข็งแรงกว่า  2 3

  4. cppreference.com, std::condition_variable::wait. ว่าด้วย wait ที่ไม่มี predicate สามารถถูกปลดบล็อกโดย spurious wakeup และว่าด้วยโอเวอร์โหลด predicate เทียบเท่ากับ while (!pred()) wait(lock); และถูกกำหนดเป็นลูปที่ถือล็อกใหม่แล้วตรวจ predicate บนการแจ้งแต่ละครั้งหรือ spurious wakeup  2

  5. Microsoft Learn, condition_variable Class. ว่าด้วย wait ที่ไม่มี predicate ถูกระบุว่าปลดบล็อกบน notify_one / notify_all และยังตื่นปลอมได้ ว่าด้วยรูป predicate wait(lock, pred) โดยพฤตินัยรัน while (!Pred()) wait(Lck); และว่าด้วย wait_for / wait_until มีคุณสมบัติเดียวกันและโอเวอร์โหลด predicate  2 3

  6. Microsoft Learn, Monitor.Wait Method. ว่าด้วย Wait ปล่อยล็อกแล้วเข้าสู่คิวรอ ว่าด้วยไม่กลับหลังถูกปลุกโดย Pulse / PulseAll จนกว่าล็อกถูกถือใหม่ และว่าด้วยการใช้งานที่ตั้งใจคือเธรดที่ถูกปลุกประเมินเงื่อนไขที่ทำให้มันเข้าสู่การรออีกครั้งและเรียก Wait อีกหากจำเป็น  2

  7. Microsoft Learn, PulseEvent function (winbase.h). ว่าด้วยเธรดที่กำลังรอสามารถถูกถอดออกจากสถานะรอชั่วคราวโดย kernel-mode APC แล้วกลับหลัง APC เสร็จ จึงหาก PulseEvent ถูกเรียกในช่วงนั้นเธรดไม่ถูกปล่อย และว่าด้วย PulseEvent จึงไม่น่าเชื่อถือและไม่ควรใช้ในแอปใหม่ ใช้ condition variable แทน  2

  8. Microsoft Learn, Using Condition Variables. ว่าด้วยตัวอย่างทางการที่อิมพลีเมนต์คิวผู้ผลิต–ผู้บริโภคด้วย critical section หนึ่งอันและ condition variable สองอัน (BufferNotEmpty และ BufferNotFull) การรอถูกทำภายในลูปที่ตรวจ predicate 

  9. Microsoft Learn, WaitOnAddress function (synchapi.h). ว่าด้วยฟังก์ชันที่รอให้ค่าของที่อยู่เปลี่ยนถูกประกันว่าจะกลับเมื่อถูกสัญญาณแต่ยังได้รับอนุญาตให้กลับด้วยเหตุอื่น ว่าด้วยตัวอย่างของการตื่นเร็วรวมสภาพหน่วยความจำต่ำ การทิ้งการปลุกก่อนหน้าสำหรับที่อยู่เดียวกัน และการรัน checked build และว่าด้วยจึงต้องเปรียบเทียบค่าอีกครั้งหลังกลับ ตัวอย่างทางการเองเป็นลูป while 

  10. Microsoft Learn, Monitor.PulseAll Method. ว่าด้วย PulseAll ย้ายเธรดจากคิวรอไปคิวพร้อม และเธรดถัดไปบนคิวพร้อมถือล็อกเมื่อล็อกถูกปล่อย และว่าด้วย Pulse / PulseAll / Wait เรียกได้เฉพาะจากภายในบล็อกซิงโครไนซ์  2

  11. Microsoft Learn, Waits and APCs. ว่าด้วย kernel APC รันแบบแย่งล่วงหน้า และระบบภายในขัดจังหวะแล้วกลับมาการรอโดยไม่กลับจาก wait API จึงสัญญาณชั่วคราวอย่าง KePulseEvent สามารถพลาดในช่วงนั้นได้ 

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. ว่าด้วยคำเตือนการวิเคราะห์สแตติกต่อการใช้ PulseEvent ว่าด้วยเธรดที่อยู่นอกการรอเพราะ APC ไม่ถูกปล่อยและแฮงตลอดไปได้ และว่าด้วยคำแนะนำสำหรับการแทนที่ด้วย SetEvent หรือออบเจ็กต์ซิงโครไนซ์อื่น 

บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง

DllMain และ Loader Lock — เหตุผลจริงที่ถูกบอกให้ "อย่าทำอะไรในการเริ่มต้น DLL"

ทำไมคุณต้องไม่เรียก LoadLibrary หรือซิงโครไนซ์กับเธรดอื่นจาก DllMain บทความนี้อธิบายจากแหล่งปฐมภูมิว่า loader lock ทำให้การแจ้ง DLL ทุกคร...

Win32 Thread Pool API — คอนเคอร์เรนซีโดยไม่สร้างเธรดเอง ด้วย CreateThreadpoolWork

โค้ดเนทีฟของคุณกระจายการเรียก CreateThread ไปทั่วหรือไม่ บทความนี้อธิบาย Win32 thread pool API ที่ออกแบบใหม่ใน Vista — ออบเจ็กต์สี่ชนิด w...

Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย

คู่มือเชิงปฏิบัติเรื่อง named pipe ซึ่งเป็นการสื่อสารระหว่างโปรเซสมาตรฐานของ Windows บทความนี้จัดระเบียบจากแหล่งปฐมภูมิ ทั้งการเลือกระหว่...

"ไม่ตอบสนอง" คืออะไรจริง ๆ — Windows ตัดสินว่าแอปแฮงอย่างไร และออกแบบอย่างไรให้ไม่แฮง

"ไม่ตอบสนอง" ของ Windows คือกลไกที่ OS ตัดสินว่าหน้าต่างไม่ได้ดึงข้อความเป็นเวลา 5 วินาทีแล้วแทนที่ด้วยหน้าต่างโกสต์ บทความนี้ครอบคลุมภาย...

แอปที่พังเมื่อรีซูมจากสลีป — อีเวนต์พลังงานของ Windows ทำงานอย่างไร และสร้างแอปธุรกิจที่รอดได้อย่างไร

เปิดแล็ปท็อปแล้วการเชื่อมต่อของแอปธุรกิจตาย — สาเหตุคือการออกแบบที่ไม่เคยคิดถึงสลีป บทความนี้ครอบคลุมลำดับการแจ้ง WM_POWERBROADCAST พฤติก...

หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ

บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้

คำถามที่พบบ่อย

คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้

spurious wakeup คือบั๊กของ OS หรือไลบรารีหรือไม่
ไม่ — เป็นพฤติกรรมที่ระบุในข้อกำหนด Win32 SleepConditionVariableCS, C++ std::condition_variable และ POSIX pthread_cond_wait ล้วนมีเอกสารทางการหรือมาตรฐานที่ระบุชัดว่าการตื่นที่ไม่ผูกกับการแจ้งสามารถเกิดได้ การอิมพลีเมนต์ที่ห้ามมันเป็นไปได้ในทางทฤษฎี แต่จะทำให้การทำงานของ condition variable ทุกครั้งช้าลง (โดยเฉพาะการแจ้งบนมัลติโปรเซสเซอร์) ดังนั้นการแลกคืออนุญาตบนความเข้าใจว่า "ความถูกต้องยังอยู่หากฝั่งที่รอตรวจเงื่อนไขอีกครั้ง" ยาจึงไม่ใช่รอการแก้ของ OS แต่เขียน wait ภายในลูป while เสมอ (หรือใช้ wait รูป predicate)
การห่อ wait ในลูป while ทำร้ายประสิทธิภาพหรือไม่
ในทางปฏิบัติต้นทุนเล็กน้อยจนไม่นับ ลูป while เพิ่มเพียงการตรวจเงื่อนไขพิเศษหนึ่งครั้งแต่ละครั้งที่คุณตื่น และนั่นคือการเปรียบเทียบราคาถูกขณะที่คุณถือล็อกอยู่แล้ว spurious wakeup เองหายาก ดังนั้นรอบลูปพิเศษเกิดเฉพาะในกรณีพิเศษ ต้นทุนของการทิ้งการตรวจเป็น if ในทางกลับกันคือ "บั๊กที่ทำซ้ำได้เฉพาะน้อยครั้ง" ที่การประมวลผลเดินต่อโดยเงื่อนไขไม่เป็นจริง — ไม่มีการเปรียบเทียบ สิ่งที่ครองต้นทุนการรอของ condition variable จริง ๆ คือการแย่งล็อกและความถี่ที่คุณแจ้ง ไม่ใช่ว่ามี while หรือไม่
หากใช้ wait รูป predicate ของ C++ ฉันลืม spurious wakeup ได้หรือไม่
สำหรับลูปรอ ใช่: cv.wait(lock, pred) โดยพฤตินัยคือ while (!pred()) wait(lock); ดังนั้นทั้ง spurious wakeup และ stolen wakeup ถูกดูดซับอัตโนมัติ โค้ด C++ ใหม่ควรใช้โอเวอร์โหลด predicate เป็นค่าเริ่มต้น คุณยังต้องปกป้องการอัปเดตสถานะที่ใช้ร่วมที่ predicate อ่านด้วย mutex เดียวกัน และฝั่งที่แจ้งยังต้องอัปเดตสถานะนั้นก่อนเรียก notify Predicate wait รับลูปแทนคุณ มันไม่รับวินัยล็อกแทนคุณ
ปัญหาเดียวกันเกิดกับ Monitor.Wait ของ C# หรือไม่
เกิด เธรดที่รอใน Monitor.Wait ถูกปลุกโดย Pulse/PulseAll แล้วถือล็อกใหม่ก่อนกลับจาก Wait แต่ในช่วงนั้นเธรดอื่นอาจถือล็อกก่อนแล้วบริโภคเงื่อนไข (stolen wakeup) เอกสารของ Microsoft ถูกเขียนบนสมมติฐานว่าเธรดที่ถูกปลุกประเมินเงื่อนไขที่ทำให้มันรออีกครั้ง และเรียก Wait อีกหากจำเป็น ดังนั้นรูปพื้นฐานใน C# ก็คือ while (!condition) Monitor.Wait(gate); ข้อจำกัดหนึ่งที่ต่างจาก Win32 คือคุณเรียก Wait/Pulse ได้เฉพาะจากภายในคำสั่ง lock
spurious wakeup เกิดด้วยเมื่อคุณรออีเวนต์ด้วย WaitForSingleObject หรือไม่
ในการรอธรรมดา (ไม่ alertable) WAIT_OBJECT_0 ถูกคืนเฉพาะเมื่อออบเจ็กต์กลายเป็น signaled จริง ไม่มีการ "ตื่นไร้เหตุ" แบบที่ condition variable มี อย่างไรก็ตาม "อีเวนต์กลายเป็น signaled" กับ "เงื่อนไขของแอปเป็นจริง" เป็นคนละเรื่อง หากผู้บริโภคหลายคนถูกปลุกโดยอีเวนต์เดียวกัน เธรดที่ถือล็อกก่อนบริโภคเงื่อนไข ดังนั้นคุณยังต้องตรวจเงื่อนไขอีกครั้งหลังตื่น การออกแบบที่พยายามสร้างการแจ้งชั่วคราวแบบ "ปลุกเฉพาะผู้ที่กำลังรอชั่วขณะนั้น" ของ condition variable ด้วยอีเวนต์ยังมักชนปัญหาความน่าเชื่อถือของ PulseEvent ดังนั้นสำหรับการรอเงื่อนไขภายในโปรเซส condition variable คือเครื่องมือที่ปลอดภัยกว่า

โปรไฟล์ผู้เขียน

หน้าแนะนำผู้เขียนบทความ

Go Komura

ผู้แทนของ KomuraSoft LLC

เชี่ยวชาญด้านการพัฒนาซอฟต์แวร์ Windows ที่ปรึกษาเทคนิค และการตรวจสอบบั๊ก โดยเฉพาะโปรเจกต์ที่มีระบบเดิมและบั๊กที่ทำซ้ำได้ยาก

ลิงก์สาธารณะ

กลับไปยังบล็อก