"ไม่ตอบสนอง" คืออะไรจริง ๆ — Windows ตัดสินว่าแอปแฮงอย่างไร และออกแบบอย่างไรให้ไม่แฮง
· Go Komura · Windows, การพัฒนา Windows, การแก้ปัญหา, มัลติเธรด, WinForms, WPF, Win32 API, การออกแบบ UI
“แอปขาวกลางการทำงานแล้วแสดง (ไม่ตอบสนอง)” “เราได้ตั๋วว่ามันแฮงเป็นครั้งคราว แต่ไม่เคยทำซ้ำได้บนเครื่องพัฒนา” — สำหรับแอปธุรกิจบน Windows “ไม่ตอบสนอง” นี้คือหนึ่งในข้อร้องเรียนที่พบบ่อยที่สุด และสิ่งที่น่าแปลกที่รู้น้อยคือสิ่งที่วางการแสดง “ไม่ตอบสนอง” ไม่ใช่แอปที่แฮงเอง — คือ OS
Windows รู้ได้อย่างไรว่าแอป “แฮง” หน้าต่างขาวเหมือนน้ำค้างแข็งนั้นคืออะไร มุ่งไปที่นักพัฒนาที่เขียนแอปธุรกิจบน Windows และเจ้าหน้าที่ไอทีที่รับตั๋วเรื่องแอปที่แฮง บทความนี้ไล่การตัดสิน “ไม่ตอบสนอง” จากพื้นฐานของลูปข้อความ และจัดระเบียบสาเหตุคลาสสิกของแฮง การออกแบบที่ไม่แฮง และขั้นตอนสอบสวนชั่วขณะที่แฮง — ทั้งหมดอิงแหล่งปฐมภูมิ
1. สรุปก่อนเลย
- “ไม่ตอบสนอง” คือการตัดสินของ OS เมื่อหน้าต่าง (และเธรด GUI ที่เป็นเจ้าของ) ไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้ดึงข้อความ (
PeekMessage) เป็นเวลา 5 วินาที OS ถือว่าไม่ตอบสนอง การตัดสินไม่ใช่รายโปรเซส1 - หน้าต่างที่ขาวคือ “หน้าต่างโกสต์” OS ซ่อนหน้าต่างเดิมแล้วสลับของปลอมที่มีตำแหน่ง ขนาด และรูปลักษณ์เดียวกันเข้ามา สิ่งที่ทำได้คือย้าย ย่อ หรือปิด เนื้อหาไม่รัน หน้าต่างโกสต์ไม่ถูกสร้างขณะมีดีบักเกอร์ติดอยู่2
- สาเหตุของแฮงเกือบเสมอรวมเหลืออย่างเดียว เธรด UI ที่ควรปั๊มลูปข้อความถูกบล็อกบนกิจหนักหรือการรอ I/O แบบซิงโครนัส การเรียกเครือข่าย การรอล็อก และ
SendMessageข้ามเธรดคือของคลาสสิก3 - หลักการออกแบบคือ “อย่ารอหรือคำนวณบนเธรด UI” ย้ายงานหนักไปเธรดเวิร์กเกอร์ (
async/await+Task.Runใน C#) และปล่อยให้เธรด UI อุทิศกับการวาด ความคืบหน้า และการรับการยกเลิก4 DoEventsและการปั๊มลูปข้อความด้วยมือคือแหล่งเพาะบั๊กการเข้าซ้ำ การแสดง “ไม่ตอบสนอง” หายไป แต่โครงสร้างตอนนี้อนุญาตให้อีเวนต์ใด ๆ ขัดกลางงาน การแยก ไม่ใช่การหลบ คือแนวทางที่ถูก- การสอบสวนเริ่มด้วยการจับสถานะที่ชั่วขณะที่แฮง ถ่ายดัมป์แล้วดูสแตกของเธรด UI และคุณระบุได้เกือบเสมอว่ามันรออะไรอยู่
2. สิ่งที่ต้องรู้ก่อน: แอป Windows ถูกขับด้วยข้อความ
เพื่อเข้าใจ “ไม่ตอบสนอง” คุณต้องรับไว้ก่อนว่าแอป GUI ของ Windows เป็นแบบขับด้วยอีเวนต์ แอป GUI ไม่ไปดึงอินพุตเอง มันรับข้อความที่ OS ส่ง (เมาส์ แป้นพิมพ์ คำขอวาดใหม่ ตัวตั้งเวลา เป็นต้น) แล้วกระทำตาม3
แต่ละเธรดที่สร้างหน้าต่างมีคิวข้อความและรันลูปข้อความแบบนี้
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // the window procedure is called
}
GetMessage ดึงข้อความจากคิว และ DispatchMessage เรียก window procedure ของหน้าต่างนั้น (ฟังก์ชันจัดการข้อความ) การจัดการคลิกปุ่ม การวาดใหม่ และตัวจัดการอีเวนต์ของ WinForms หรือ WPF ทั้งหมด เมื่อต้มลง รันภายในหนึ่งรอบของลูปนี้5
flowchart TB
accTitle: โครงสร้างพื้นฐานของลูปข้อความ
accDescr: OS วางเมาส์ แป้นพิมพ์ และอินพุตอื่นเข้าสู่คิวข้อความของเธรด ลูปของเธรด UI ดึงด้วย GetMessage เรียก window procedure ด้วย DispatchMessage และกลับสู่ยอดลูปเมื่อการประมวลผลเสร็จ
os["OS (อินพุต คำขอวาดใหม่ ตัวตั้งเวลา)"] --> q["คิวข้อความของเธรด"]
q --> gm["ดึงด้วย GetMessage"]
gm --> dm["DispatchMessage"]
dm --> wp["จัดการใน window procedure"]
wp --> gm
ภาพ 1: หัวใจของแอป GUI คือลูปข้อความ ตัวจัดการอีเวนต์ทุกตัวรันเป็นหนึ่งรอบของลูปนี้
โครงสร้างนี้มีผลสำคัญอย่างหนึ่ง หากคุณทำงานที่ใช้เวลานานภายใน window procedure (ตัวจัดการอีเวนต์) ลูปดึงข้อความถัดไปไม่ได้ในระหว่างนั้น มันตอบสนองทั้งต่อคลิกและต่อคำขอวาดใหม่ไม่ได้ — นั่นคือสิ่งที่ “แฮง” เป็นจริง ๆ
ยังคุ้มที่จะรับไว้ว่าข้อความถูกส่งด้วยสองเส้นทาง PostMessage วางข้อความบนคิวแล้วกลับทันที และลูปดึงแล้วประมวลผลข้อความตามลำดับ SendMessage ในทางกลับกัน เรียก window procedure โดยตรงและไม่กลับสู่ผู้เรียกจนกว่าการประมวลผลเสร็จ36 ความต่างนั้นพาตรงเข้าสู่การอภิปรายเดดล็อกในบทที่ 4
flowchart TB
accTitle: สองเส้นทางการส่งข้อความ
accDescr: PostMessage วางข้อความบนคิวแล้วกลับทันที ลูปข้อความดึงแล้วประมวลผลตามลำดับ SendMessage เรียก procedure โดยตรงและไม่กลับสู่ผู้เรียกจนกว่าการประมวลผลเสร็จ
pm["PostMessage"] --> q2["วางบนคิว (กลับทันที)"]
q2 --> loop["ลูปดึงแล้วประมวลผลตามลำดับ"]
sm["SendMessage"] --> direct["เรียก procedure โดยตรง"]
direct --> w2["ไม่กลับจนกว่าการประมวลผลเสร็จ"]
ภาพ 2: แม้ทั้งคู่ “ส่งข้อความ” Post ที่เข้าคิวกับ Send ที่รอความเสร็จมีธรรมชาติต่างกันโดยสิ้นเชิง
3. “ไม่ตอบสนอง” ถูกตัดสินอย่างไร — กฎ 5 วินาทีกับหน้าต่างโกสต์
แล้ว OS รู้ได้อย่างไรว่า “แอปนี้แฮง” เกณฑ์ถูกบันทึกอย่างเป็นทางการ OS ถือว่าหน้าต่างไม่ตอบสนองเมื่อมันไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้เรียก PeekMessage (การดึงข้อความ) เป็นเวลา 5 วินาที1 กล่าวอีกนัย OS เฝ้าว่า “ลูปข้อความกำลังหมุนจริงหรือไม่” แบบที่คุณจับชีพจร และหากไม่มีชีพจรเป็นเวลา 5 วินาทีมันตัดสินว่าหน้าต่างไม่ตอบสนอง (เอกสารระบุว่าค่า 5 วินาทีนี้อาจเปลี่ยนในอนาคต) หน่วยของการตัดสินคือหน้าต่างและเธรด GUI ที่เป็นเจ้าของ ในแอปที่มีหลายเธรด UI การแฮงของเธรดหนึ่งไม่หมายความว่าหน้าต่างบนอีกเธรดตาย เธรดที่ต้องดูในดัมป์คือเจ้าของหน้าต่างที่แฮง
สิ่งที่เกิดกับหน้าต่างระดับบนที่ถูกตัดสินก็ถูกบันทึกเช่นกัน OS ซ่อนหน้าต่างเดิมแล้วแทนที่ด้วย “หน้าต่างโกสต์” ที่มีลำดับ Z ตำแหน่ง ขนาด และรูปลักษณ์เดียวกัน สิ่งที่ผู้ใช้ทำได้คือย้าย ปรับขนาด หรือปิด (บังคับ) แอปข้างในไม่ได้ตอบสนองจริง ดังนั้นการทำงานอื่นใช้ไม่ได้2
flowchart TB
accTitle: การตัดสินหน้าต่างแฮงและการสลับหน้าต่างโกสต์
accDescr: เมื่อเธรด UI ถูกบล็อกบนกิจหนักและการดึงข้อความหยุดเป็นเวลา 5 วินาที OS ตัดสินว่าหน้าต่างไม่ตอบสนอง ซ่อนของเดิม สลับหน้าต่างโกสต์รูปลักษณ์เดียวกันเข้ามา และเสนอให้ผู้ใช้เฉพาะย้าย ย่อ และปิด
busy["เธรด UI ถูกบล็อกบนกิจหนัก"] --> stop["การดึงข้อความหยุด"]
stop --> judge{"ผ่าน 5 วินาที?"}
judge -->|"ไม่"| stop
judge -->|"ใช่"| ghost["สลับหน้าต่างโกสต์เข้ามา"]
ghost --> u1["ชื่อแสดง (ไม่ตอบสนอง)"]
ghost --> u2["ขาวเหมือนน้ำค้างแข็ง ย้ายและปิดได้อย่างเดียว"]
ภาพ 3: ทั้งข้อความ “ไม่ตอบสนอง” และหน้าจอขาวเป็นของหน้าต่างโกสต์ที่ OS สลับเข้ามา ไม่ใช่ของแอปที่แฮง
สตริง “(ไม่ตอบสนอง)” ที่ปรากฏบนแถบชื่อ และลักษณะขาวเหมือนน้ำค้างแข็งภายใต้ธีม Aero ทั้งคู่เป็นของหน้าต่างโกสต์นี้ ผลเชิงปฏิบัติสองอย่างตามมา
- เมื่อถึงเวลาที่ “ไม่ตอบสนอง” ถูกแสดง เธรดที่เป็นเจ้าของหน้าต่างนั้นไม่ได้ประมวลผลข้อความอย่างน้อย 5 วินาที ไม่ใช่ว่า “การแสดงมาเร็วเกินไป” — เธรด UI ถูกบล็อกแน่นอน
- หน้าต่างโกสต์ไม่ถูกสร้างขณะมีดีบักเกอร์ติดอยู่2 เมื่อดูราวกับว่า “มันไม่เคยไปไม่ตอบสนองภายใต้ดีบักเกอร์ แต่ไปในรีลีส” แฮงเองอาจเหมือนกันและมีเพียงการแสดงที่ต่าง
ยังมี API DisableProcessWindowsGhosting ที่ปิดการสลับนี้สำหรับทั้งโปรเซส7 มันมีไว้สำหรับกรณีพิเศษ เช่น เทอร์มินัลคีออสก์ที่คุณไม่อยากให้ OS ทำให้หน้าต่างดูใช้งานได้ด้วยตนเอง การเรียกมันหยุดการแสดง “ไม่ตอบสนอง” ไม่ให้ปรากฏ แต่ความจริงที่แอปแฮงไม่เปลี่ยน ให้เข้าใจว่านี่ไม่ใช่สิ่งที่แอปทั่วไปใช้เป็นมาตรการ “ไม่ตอบสนอง”
4. ทำไมแอปจึงแฮง — รูปแบบคลาสสิกที่บล็อกเธรด UI
สาเหตุ เมื่อต้มลง คือจุดเดียว — “เธรด UI ไม่กลับสู่ลูปข้อความ” — แต่รูปที่คุณพบในทางปฏิบัติตกเป็นของคลาสสิกไม่กี่อย่าง
flowchart TB
accTitle: การจัดประเภทสาเหตุคลาสสิกที่บล็อกเธรด UI
accDescr: สี่ตระกูลคลาสสิก — I/O แบบซิงโครนัสและการเรียกเครือข่าย การรอล็อก SendMessage ข้ามเธรด และการเกี่ยวข้องของ COM STA — ทั้งหมดรวมเหลือจุดเดียวที่เธรด UI กลับสู่ลูปข้อความไม่ได้
kind{"สาเหตุคลาสสิกใด?"}
kind --> io{"I/O หรือล็อก?"}
kind --> other{"SendMessage หรือ COM?"}
io --> c1["I/O ซิงค์และเครือข่าย"]
io --> c2["การรอล็อก"]
other --> c3["SendMessage"]
c3 -.-> c3n["ข้ามเธรด"]
other --> c4["การเกี่ยวข้องของ COM STA"]
c1 --> core["UI กลับไม่ได้"]
c2 --> core
c3 --> core
c4 --> core
core --> ar["การตรวจไม่ตอบสนอง"]
ภาพ 4: อาการที่เห็นเหมือนกัน แต่ตัวการที่บล็อกเธรดตกเป็นสี่ตระกูล และมาตรการต่างกันสำหรับแต่ละอย่าง
I/O แบบซิงโครนัสและการเรียกเครือข่าย นี่พบบ่อยที่สุด รูปแบบของการทำแบบซิงโครนัส ภายในตัวจัดการคลิกปุ่ม การอ่านหรือเขียนไฟล์ใหญ่ คิวรีฐานข้อมูล การเรียก Web API หรือการเข้าถึงไฟล์บนไดรฟ์เครือข่าย บนเครื่องพัฒนามันเสร็จในเสี้ยววินาที จึงไม่สังเกต ความหน่วงเครือข่ายในโปรดักชันหรือสะดุดของไฟล์เซิร์ฟเวอร์เปลี่ยนเป็นการรอหลายสิบวินาที และคุณได้ตั๋วว่า “มันแฮงเป็นครั้งคราว” ไดรฟ์เครือข่ายมีหมดเวลานานเมื่อการเชื่อมต่อขาด และทำให้อาการแย่ลงอย่างมาก
การรอล็อก รูปแบบที่เธรด UI พยายามถือล็อกบนข้อมูลที่ใช้ร่วมกับเธรดเวิร์กเกอร์ แล้วลงเอยด้วยการรอเวิร์กเกอร์ที่ถือล็อกนั้นนาน วินัยล็อกมีรายละเอียดในซีรีส์มัลติเธรดเชิงปฏิบัติ
SendMessage ข้ามเธรด SendMessage ไม่กลับจนกว่า procedure ของหน้าต่างปลายทางประมวลผลเสร็จ6 เมื่อคุณส่งไปยังหน้าต่างบนอีกเธรด ผู้ส่งถูกทำให้รอจนกว่าเธรดนั้นอยู่ในสถานะที่ประมวลผลข้อความได้ หากเธรดปลายทางเองกำลังรออะไรอยู่ คุณได้เดดล็อกข้อความที่แต่ละฝั่งรออีกฝั่ง3 การส่งไปยัง HWND_BROADCAST โดยเฉพาะจะลากคุณเข้าทันทีที่หน้าต่างเดียวไม่ตอบสนอง เมื่อคุณรอไม่ได้ ให้พิจารณา SendMessageTimeout หรือ PostMessage ซึ่งไม่รอคำตอบ8
sequenceDiagram
accTitle: เดดล็อกที่เกิดจาก SendMessage ข้ามเธรด
accDescr: หากเธรดเวิร์กเกอร์ส่ง SendMessage ไปยังหน้าต่างของเธรด UI ขณะที่เธรด UI ถูกบล็อกรอผลของเวิร์กเกอร์ แต่ละฝั่งรอให้อีกฝั่งเสร็จแล้วคุณได้เดดล็อก
participant U as เธรด UI
participant W as เธรดเวิร์กเกอร์
U->>U: รอให้เวิร์กเกอร์เสร็จ (ถูกบล็อก)
W->>U: SendMessage (ไม่กลับจนกว่าประมวลผล)
Note over U: ประมวลผลข้อความไม่ได้ (ถูกบล็อก)
Note over W: กลับจาก SendMessage ไม่ได้
Note over U,W: รอกันและกัน — เดดล็อก
ภาพ 5: “เธรด UI รอเวิร์กเกอร์ และเวิร์กเกอร์รอเธรด UI ผ่าน SendMessage” คือเดดล็อกคลาสสิก
การเกี่ยวข้องของ COM apartment การเรียกไปยังออบเจ็กต์ STA ถูกส่งเป็นข้อความหน้าต่าง ดังนั้นเมื่อเธรด UI (STA) ถูกบล็อก การเรียก COM จากเธรดอื่นถูกบล็อกเป็นผลข้างเคียงด้วย โครงสร้างนั้นอธิบายในบทความ COM STA/MTA
กองของ “มันแค่ชั่วขณะ” แม้การเรียกซิงโครนัส 50 มิลลิวินาที ที่ถูกเรียก 100 ครั้งในลูป ก็คือ 5 วินาที เกณฑ์ไม่ตอบสนองคือ 5 วินาที แต่ “ความเชื่องช้า” ที่รู้สึกได้เริ่มราว 100 มิลลิวินาที กฎนิ้วหัวแม่มือของการออกแบบคือ “เธรด UI ถูกบล็อกได้เฉพาะมิลลิวินาที”
5. การออกแบบที่ไม่แฮง — ย้ายงานหนักออกจากเธรด UI
หลักการออกแบบมีอย่างเดียว: ย้ายงานที่ใช้เวลานานออกจากเธรด UI ใน C# (WinForms/WPF) async/await คือแนวทางที่ถูกที่สั้นที่สุด
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU-heavy work, or APIs that are only synchronous, go to a worker via Task.Run
var result = await Task.Run(() => HeavyCalculation(input));
// For I/O, use APIs that are natively async (they do not consume a thread either)
var data = await httpClient.GetStringAsync(url);
// After await you are back on the UI thread, so you can touch controls directly
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// An exception leaking from an async void handler will take the app down. Catch it here
MessageBox.Show($"The operation failed: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
มีสามจุด ประการแรก ขณะ await กำลังรอ เธรด UI กลับอยู่ในลูปข้อความ จึงไม่ไปไม่ตอบสนอง ประการที่สอง ความต่อเนื่องหลัง await กลับสู่เธรด UI จึงแตะคอนโทรลตามปกติหลังจากนั้นได้ (การแตะคอนโทรลโดยตรงจากเธรดเวิร์กเกอร์ถูกห้าม หากจำเป็น ใช้ Control.Invoke / Dispatcher.InvokeAsync)4 ประการที่สาม ปิดปุ่มขณะงานกำลังรัน และมิฉะนั้นฆ่าการเข้าซ้ำด้วยการออกแบบ
ภาพเหมือนกันใน Win32 เนทีฟ: ส่งงานให้เธรดเวิร์กเกอร์ แจ้งเธรด UI เรื่องความเสร็จเป็นข้อความที่กำหนดเองผ่าน PostMessage และอัปเดต UI ใน window procedure PostMessage เพียงวางข้อความบนคิวแล้วกลับทันที ดังนั้นฝั่งเวิร์กเกอร์ก็ไม่ถูกบล็อกเช่นกัน6 เขียนการรอเธรดเวิร์กเกอร์เองด้วยวินัยที่ครอบในบทความเรื่อง condition variable
flowchart TB
accTitle: การแบ่งบทบาทในแอปที่ไม่แฮง
accDescr: เธรด UI รับผิดชอบเฉพาะการรับอินพุต การแสดงความคืบหน้า และการรับการยกเลิก เธรดเวิร์กเกอร์รันงานหนักแล้วคืนความเสร็จสู่เธรด UI ผ่าน PostMessage หรือความต่อเนื่องของ await
ui["เธรด UI: อินพุต ความคืบหน้า ยกเลิก"] -->|"ส่งต่องาน"| w["เธรดเวิร์กเกอร์: งานหนัก"]
w -->|"PostMessage / ความต่อเนื่องของ await"| ui
ui -.-> ng["ไม่มี I/O ซิงค์หรือการคำนวณยาวบนเธรด UI"]
ภาพ 6: คงเธรด UI เป็น “โต๊ะรับ” ส่งงานหนักให้เวิร์กเกอร์เสมอ และรับเฉพาะการแจ้งความเสร็จ
สิ่งที่อยากหลีกเลี่ยงคือเทคนิคของการแทรก Application.DoEvents() หรือลูป PeekMessage ระหว่างชิ้นงานหนักเพียงเพื่อให้การแสดงมีชีวิต คุณหลบไม่ตอบสนอง แต่ตัวจัดการอีเวนต์ใด ๆ เข้าซ้ำ กลางงาน การคลิกปุ่มครั้งที่สอง การปิดฟอร์มระหว่างประมวลผล ตัวตั้งเวลายิง — อย่างใดอย่างหนึ่งสามารถทำให้ข้อมูลที่ยังกำลังประมวลผลเสีย และบั๊กขึ้นกับจังหวะและทำซ้ำยาก คงการปั๊มลูปข้อความด้วยมือภายในโครงสร้างจำกัด เช่น กล่องโต้ตอบความคืบหน้าแบบโมดัล และตามกฎแก้ด้วยการแยก
sequenceDiagram
accTitle: ไทม์ไลน์ของบั๊กการเข้าซ้ำที่เกิดจาก DoEvents
accDescr: การเรียก DoEvents กลางงานหนักให้อีเวนต์คลิกที่อยู่ในคิวขัดและรัน เขียนทับข้อมูลที่ยังกำลังประมวลผล แล้วกลับไปงานเดิม ก่อการเสียข้อมูลที่ขึ้นกับจังหวะ
participant U as เธรด UI
U->>U: งานหนักเริ่ม (ข้อมูลกำลังประมวลผล)
U->>U: DoEvents (ประมวลผลข้อความในคิว)
Note over U: ตัวจัดการคลิกปุ่มซ้ำขัดเข้ามา
U->>U: งานที่ขัดเขียนทับข้อมูล
U->>U: งานเดิมกลับมา (ข้อมูลไม่สอดคล้องแล้ว)
ภาพ 7: DoEvents ลบ “ไม่ตอบสนอง” แลกกับการเชิญอีเวนต์ใด ๆ เข้ากลางงาน
สำหรับงานที่รันนาน ให้รวมการแสดงความคืบหน้าและการยกเลิกในการออกแบบด้วย ส่งความคืบหน้าไปยัง UI ด้วย IProgress<T> และสื่อสารการขัดจังหวะด้วย CancellationToken แล้วผู้ใช้เห็นว่า “มันกำลังทำงาน” และจะไม่เอื้อมไปยุติบังคับ (ซึ่งมักเป็นสาเหตุของการเสียข้อมูล)
flowchart TB
accTitle: ลำดับความคืบหน้าและการยกเลิกสำหรับงานที่รันนาน
accDescr: เธรดเวิร์กเกอร์ส่งความคืบหน้าไปยังเธรด UI ผ่าน IProgress การยกเลิกบน UI ถึงเวิร์กเกอร์ผ่าน CancellationToken เวิร์กเกอร์หยุดที่ขอบที่สะดวกแล้วทำความสะอาด
w3["เวิร์กเกอร์: งานที่รันนาน"] -->|"ความคืบหน้าผ่าน IProgress"| ui2["UI: ความคืบหน้าและปุ่มหยุด"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["หยุดที่ขอบแล้วทำความสะอาด"]
ภาพ 8: ความคืบหน้าคือ “เวิร์กเกอร์ → UI” การยกเลิกคือ “UI → เวิร์กเกอร์” รวมช่องสองทางบางนี้ในการออกแบบตั้งแต่ต้น
6. สอบสวนชั่วขณะที่แฮง
ในการสอบสวน “มันแฮงเป็นครั้งคราว” สิ่งที่มีค่าที่สุดคือสถานะเธรดที่ชั่วขณะพอดีที่มันแฮง รีบูต แล้วหลักฐานหาย
ถ่ายดัมป์ บนแท็บรายละเอียดของตัวจัดการงาน คลิกขวาโปรเซสเป้าหมาย → “สร้างไฟล์ดัมป์” แค่นั้นให้ดัมป์เต็มพร้อมสแตกของทุกเธรด การบอกเจ้าหน้าที่ไอทีที่รับตั๋วว่า “เมื่อมันแฮง ให้ถ่ายสิ่งนี้ก่อนปิด” เปลี่ยนอัตราความสำเร็จของการสอบสวนมาก สำหรับการสร้างกลไกเก็บ ดูบทความเรื่องการเก็บ crash dump
ดูสแตกของเธรด UI เปิดดัมป์ใน WinDbg แล้วดูสแตกของเธรดที่กำลังปั๊มลูปข้อความ (มักเป็นเธรด 0) I/O แบบซิงโครนัสปรากฏเป็น ReadFile หรือ API เครือข่าย การรอล็อกเป็นเรียกตระกูล WaitFor… และ SendMessage ข้ามเธรดเป็นการรอภายใน SendMessage — ตามที่เป็น วิธีอ่านอธิบายในบทความแนะนำ WinDbg
ดูแบบมีชีวิต ด้วย Process Explorer คุณตรวจรายการเธรดและสแตกได้ทันที เมื่อมันเชื่องช้าอย่างคงที่ ให้ถ่ายเทรซ WPR แล้ววิเคราะห์การรอของเธรด UI ตามเวลา (WPR/WPA ในทางปฏิบัติ)
flowchart TB
accTitle: ขั้นตอนพื้นฐานสำหรับการสอบสวนไม่ตอบสนอง
accDescr: ถ่ายดัมป์ที่ชั่วขณะที่แฮง ดูสแตกของเธรด UI ระบุว่าหยุดบน I/O แบบซิงโครนัส การรอล็อก หรือ SendMessage ข้ามเธรด แล้วเชื่อมโยงกับการแก้การออกแบบที่ตรงกัน
hang["ชั่วขณะที่แฮง"] --> dump["ถ่ายดัมป์ (ก่อนปิด)"]
dump --> stack["ดูสแตกของเธรด UI"]
stack --> io["I/O ซิงค์หรือการรอเครือข่าย"]
stack --> lock["การรอล็อก"]
stack --> sm["SendMessage ข้ามเธรด"]
io -.-> fix["แยกจุดไปเวิร์กเกอร์"]
lock -.-> fix
sm -.-> fix
ภาพ 9: ดาวของการสอบสวนคือ “ดัมป์ของชั่วขณะที่แฮง” สแตกของเธรด UI เองคือการจัดประเภทสาเหตุ
คุณยังทำให้วิธีตัดครั้งแรกจากอาการเป็นมาตรฐานได้ หากมันแฮงเสมอบนการทำงานหนึ่ง ให้สงสัย I/O แบบซิงโครนัสภายในตัวจัดการนั้นก่อน หากมันแฮงน้อยและไม่สัมพันธ์กับการทำงาน ให้สงสัยลำดับล็อกหรือเดดล็อก SendMessage ข้ามเธรด และจับคู่เป้าหมายการรอของทั้งสองเธรดในดัมป์ หากมันแฮงเฉพาะในสภาพแวดล้อมหนึ่ง ให้สงสัยหมดเวลาจากปัจจัยสภาพแวดล้อม เช่น ไดรฟ์เครือข่าย พร็อกซี หรือซอฟต์แวร์แอนตี้ไวรัส
7. สรุป
- “ไม่ตอบสนอง” คือกลไกที่ OS ตัดสินว่าแอปไม่ได้ดึงข้อความเป็นเวลา 5 วินาทีแล้วสลับหน้าต่างโกสต์เข้ามา สิ่งที่วางการแสดงคือ OS ไม่ใช่แอป
- สาเหตุของแฮงคือจุดเดียว: “เธรด UI กลับสู่ลูปข้อความไม่ได้” I/O แบบซิงโครนัส เครือข่าย การรอล็อก และ SendMessage ข้ามเธรดคือของคลาสสิก
- มาตรการคือย้ายงานหนักออกจากเธรด UI ใน C# คือ
async/await+Task.Runใน Win32 คือเธรดเวิร์กเกอร์ +PostMessageกันการเข้าซ้ำระหว่างรันด้วยการออกแบบ เช่น ปิดปุ่ม - การหลบด้วย
DoEventsแลกกับบั๊กการเข้าซ้ำDisableProcessWindowsGhostingเพียงลบการแสดง ไม่มีอย่างใดเป็นการแก้ที่ราก - สำหรับการสอบสวน ดัมป์ของ “ชั่วขณะที่แฮง” คือสิ่งสำคัญที่สุด สาเหตุเกือบเสมอถูกเขียนบนสแตกของเธรด UI ตามที่เป็น
จากมุมของผู้ใช้ “ไม่ตอบสนอง” คือ “มันพัง” แต่เมื่อคุณรู้กลไก คุณแปลเป็นประโยคที่แม่นได้ว่า “เธรด UI ไม่กลับมาเป็นเวลา 5 วินาที” ทำงานย้อนจากประโยคนั้น ผู้สมัครสาเหตุ การแก้ และขั้นตอนสอบสวนทั้งหมดหลุดออกมาตามธรรมชาติ
บทความที่เกี่ยวข้อง
- Spurious Wakeup — ทำไม condition variable จึงตื่น “โดยไม่ถูกแจ้ง” และวิธีรออย่างถูกต้องบน Windows
- แนวปฏิบัติมัลติเธรดเชิงปฏิบัติ: ฉบับ .NET — สิ่งที่ต้องตัดสินก่อนเพิ่มเธรด
- พื้นฐาน COM STA/MTA - โมเดลเธรดและวิธีหลีกเลี่ยงแฮง
- อ่าน Crash Dump ด้วย WinDbg + SOS — คู่มือวิเคราะห์เชิงปฏิบัติหลังการเก็บ
- Process Explorer / Handle / VMMap ในทางปฏิบัติ — ตามแฮง รั่ว และ “ไฟล์กำลังถูกใช้” จากสถานะตอนนี้
- Windows Shutdown จากมุมของแอป — รอดจากการแจ้งออก รีสตาร์ต และการตัดไฟอย่างถูกต้อง
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับการสอบสวนหาสาเหตุของแอปธุรกิจที่ “แฮงเป็นครั้งคราว” หรือไป “ไม่ตอบสนอง” (การวิเคราะห์ดัมป์และการวิเคราะห์เทรซ) การรีแฟกเตอร์โค้ด UI เดิมที่เต็มไปด้วยงานซิงโครนัสไปสู่ async/await และการแยกเธรดเวิร์กเกอร์ และการตรวจทานการออกแบบ UI ที่ไม่แข็ง แม้เมื่อคุณยังไม่มีขั้นตอนทำซ้ำ เราช่วยได้ตั้งแต่การออกแบบวิธีเก็บหลักฐาน
- การตรวจสอบบั๊กและวิเคราะห์หาสาเหตุที่แท้จริง
- ที่ปรึกษาด้านเทคนิคและการตรวจทานการออกแบบ
- การพัฒนาแอปพลิเคชัน Windows
- ติดต่อเรา
ลิงก์อ้างอิง
-
Microsoft Learn, IsHungAppWindow function (winuser.h). ว่าด้วยเกณฑ์การตัดสินที่แอปถูกถือว่าไม่ตอบสนองเมื่อมัน “ไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้เรียก PeekMessage เป็นเวลาหมดเวลาภายใน 5 วินาที” ว่าด้วยเกณฑ์ 5 วินาทีนี้อาจเปลี่ยน และว่าด้วยฟังก์ชันคืน TRUE เสมอสำหรับหน้าต่างโกสต์ ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). ว่าด้วยระบบถือว่าหน้าต่างระดับบนไม่ตอบสนองเมื่อมันหยุดตอบสนองต่อข้อความหลายวินาทีแล้วแทนที่ด้วยหน้าต่างโกสต์ที่มีลำดับ Z ตำแหน่ง ขนาด และรูปลักษณ์เดียวกัน ว่าด้วยผู้ใช้ย้าย ปรับขนาด หรือปิดได้อย่างเดียว และว่าด้วยหน้าต่างโกสต์ไม่ถูกสร้างขณะมีดีบักเกอร์ติดอยู่ ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. ว่าด้วยแอป Windows เป็นแบบขับด้วยอีเวนต์และ window procedure ประมวลผลข้อความ ว่าด้วยความต่างระหว่างข้อความในคิวกับข้อความที่ส่งโดยตรง ว่าด้วยการสลับหน้าต่างที่ไม่ตอบสนองเป็นหน้าต่างโกสต์ และว่าด้วยตอนที่ครอบเดดล็อกจากเธรดที่ส่งข้อความหากัน ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). ว่าด้วยคอนโทรล WinForms ไม่ปลอดภัยที่จะแตะจากเธรดอื่นนอกจากเธรดที่สร้างพวกมัน ว่าด้วยการใช้ Invoke/BeginInvoke สำหรับการอัปเดตจากอีกเธรด และว่าด้วยรูปแบบอะซิงโครนัสที่ปลอดภัยโดยใช้ async/await หรือ BackgroundWorker ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. ว่าด้วยการอิมพลีเมนต์ลูปข้อความทั่วไปด้วย GetMessage, TranslateMessage และ DispatchMessage และวิธีตรวจคิวข้อความ ↩
-
Microsoft Learn, SendMessage function (winuser.h). ว่าด้วย SendMessage เรียก window procedure ของหน้าต่างที่ระบุและไม่กลับจนกว่าการประมวลผลเสร็จ ว่าด้วยการส่งไปยังหน้าต่างบนอีกเธรดทำให้ผู้ส่งรอจนกว่าเธรดนั้นประมวลผลข้อความ และว่าด้วยความต่างจาก PostMessage ซึ่งวางข้อความบนคิวโดยไม่รอคำตอบ ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). ว่าด้วยสามารถปิด สำหรับโปรเซส GUI ที่เรียก ฟีเจอร์หน้าต่างโกสต์ที่ทำให้หน้าต่างที่ไม่ตอบสนองย่อ ย้าย และปิดได้ และว่าด้วยการปิดมีอยู่ตลอดอายุโปรเซส ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). ว่าด้วยสามารถส่งข้อความพร้อมหมดเวลา และว่าด้วยแฟล็ก (SMTO_ABORTIFHUNG) ที่กลับโดยไม่รอเมื่อหน้าต่างไม่ตอบสนอง (ถูกตัดสินว่าแฮง) ↩
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
DllMain และ Loader Lock — เหตุผลจริงที่ถูกบอกให้ "อย่าทำอะไรในการเริ่มต้น DLL"
ทำไมคุณต้องไม่เรียก LoadLibrary หรือซิงโครไนซ์กับเธรดอื่นจาก DllMain บทความนี้อธิบายจากแหล่งปฐมภูมิว่า loader lock ทำให้การแจ้ง DLL ทุกคร...
Win32 Thread Pool API — คอนเคอร์เรนซีโดยไม่สร้างเธรดเอง ด้วย CreateThreadpoolWork
โค้ดเนทีฟของคุณกระจายการเรียก CreateThread ไปทั่วหรือไม่ บทความนี้อธิบาย Win32 thread pool API ที่ออกแบบใหม่ใน Vista — ออบเจ็กต์สี่ชนิด w...
แอปที่พังเมื่อรีซูมจากสลีป — อีเวนต์พลังงานของ Windows ทำงานอย่างไร และสร้างแอปธุรกิจที่รอดได้อย่างไร
เปิดแล็ปท็อปแล้วการเชื่อมต่อของแอปธุรกิจตาย — สาเหตุคือการออกแบบที่ไม่เคยคิดถึงสลีป บทความนี้ครอบคลุมลำดับการแจ้ง WM_POWERBROADCAST พฤติก...
Spurious Wakeup — ทำไม condition variable จึงตื่น "โดยไม่ถูกแจ้ง" และวิธีรออย่างถูกต้องบน Windows
การรอของ condition variable สามารถกลับได้แม้ไม่มีการแจ้งมาถึง (spurious wakeup) บทความนี้อธิบายจากการอิมพลีเมนต์ของ Windows ว่าทำไมข้อกำห...
คลิปบอร์ดกับลากและวางทำงานอย่างไร — จัดการการถ่ายโอนข้อมูล OLE ให้ถูกต้องในแอปธุรกิจ
วางตาราง Excel แล้วการจัดรูปแบบพัง ปิดแอปต้นทางแล้ววางไม่ได้อีก — ทั้งคู่มาจากคลิปบอร์ดที่วางเนื้อหาเดียวกันในหลายรูปแบบพร้อมกัน บทความนี...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
เธรด UI และตัวจับเวลา
เธรด UI ของ WPF / WinForms, โฟลว์อะซิงโครนัส, Dispatcher และการออกแบบตัวจับเวลา
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- ภายใต้เงื่อนไขใด "ไม่ตอบสนอง" จึงปรากฏ
- OS ตัดสินว่าหน้าต่างแฮงเมื่อแอปที่มีหน้าต่างไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้ดึงข้อความ (PeekMessage) เป็นเวลา 5 วินาที หน้าต่างระดับบนที่แฮงถูกซ่อนและถูกแทนที่ด้วย "หน้าต่างโกสต์" ที่มีตำแหน่ง ขนาด และรูปลักษณ์เดียวกัน ข้อความแถบชื่อ "(ไม่ตอบสนอง)" และลักษณะขาวเหมือนน้ำค้างแข็งเป็นของหน้าต่างโกสต์นี้ ซึ่งให้คุณย้าย ย่อ หรือปิดได้อย่างเดียว กล่าวอีกนัย "ไม่ตอบสนอง" ไม่ใช่สิ่งที่แอปเองแสดง — เป็นหน้าจอที่ OS วางแทนแอป
- มีการตั้งค่าให้ "ไม่ตอบสนอง" ไม่ปรากฏขณะงานกำลังดำเนินหรือไม่
- การเรียก DisableProcessWindowsGhosting ปิดการสลับไปหน้าต่างโกสต์สำหรับโปรเซสนั้น แต่สิ่งนั้นเพียงทำให้แฮงมองเห็นได้น้อยลงสำหรับผู้ใช้ — หน้าต่างยังไม่ตอบสนองต่ออินพุต และจากมุมของผู้ใช้มันคือการแข็งทั้งก้อนโดยไม่มีทางย้ายหรือปิด การแก้จริงไม่ใช่การยับยั้งการแสดง แต่เป็นการย้ายงานหนักไปเธรดเวิร์กเกอร์เพื่อให้เธรด UI ไม่ถูกบล็อกแม้หนึ่งในสิบวินาที อย่าว่าแต่ห้าวินาที โปรดทราบด้วยว่า OS ไม่สร้างหน้าต่างโกสต์ขณะมีดีบักเกอร์ติดอยู่ จึงดูราวกับว่า "ไม่ตอบสนอง" ไม่เกิดระหว่างการดีบัก
- ยอมรับได้หรือไม่ที่จะหลีกเลี่ยง "ไม่ตอบสนอง" ด้วย DoEvents (การปั๊มลูปข้อความด้วยมือ)
- ไม่แนะนำ การหมุน DoEvents หรือลูป PeekMessage กลางงานหนักจะหลบการตัดสินหน้าต่างแฮง แต่ตัวจัดการอีเวนต์ใด ๆ ก็เข้าซ้ำได้ — การคลิกปุ่มครั้งที่สอง การปิดหน้าต่าง ตัวตั้งเวลา เป็นต้น ตัวจัดการอื่นที่เขียนทับข้อมูลที่ยังกำลังประมวลผล หรือแตะฟอร์มที่ควรปิดแล้วโยน ก่อบั๊กการเข้าซ้ำที่ขึ้นกับจังหวะและทำซ้ำยาก — แย่กว่า "ไม่ตอบสนอง" เอง แนวทางที่ถูกคือย้ายงานเองไปเธรดเวิร์กเกอร์ด้วย Task.Run หรือคล้ายกัน และปล่อยให้เธรด UI รับผิดชอบเฉพาะการแสดงความคืบหน้าและการรับการยกเลิก
- จะอัปเดต UI (คอนโทรล) จากเธรดเวิร์กเกอร์อย่างไร
- คอนโทรล WinForms และอิลิเมนต์ WPF แตะได้เฉพาะจากเธรดที่สร้างพวกมัน (ปกติคือเธรด UI) การแตะโดยตรงจากเธรดเวิร์กเกอร์ก่อข้อยกเว้นหรือพฤติกรรมไม่กำหนด ใน C# async/await คือเส้นทางที่ง่ายที่สุด: ความต่อเนื่องหลัง await กลับสู่เธรด UI ที่เรียก จึงอัปเดตคอนโทรลตามปกติหลัง await ได้ หากจะสลับอย่างชัดเจน ใช้ Control.Invoke/BeginInvoke ใน WinForms และ Dispatcher.InvokeAsync ใน WPF ใน Win32 เนทีฟ รูปแบบที่ลงตัวคือให้เธรดเวิร์กเกอร์ PostMessage ข้อความความเสร็จที่กำหนดเองไปยังเธรด UI และให้ window procedure อัปเดต UI
- จะสอบสวนว่าทำไมแอปจึงแสดง "ไม่ตอบสนอง" อย่างไร
- สิ่งสำคัญคือจับสถานะที่ "ชั่วขณะเอง" ที่แฮง ก่อนอื่นถ่ายดัมป์เต็มจากแท็บรายละเอียดของตัวจัดการงานด้วย "สร้างไฟล์ดัมป์" แล้วใน WinDbg ดูสแตกของเธรด UI (เธรดที่รันลูปข้อความ) ว่าติดใน I/O แบบซิงโครนัส การรอเครือข่าย การรอล็อก หรือการรอเธรดอื่นผ่าน SendMessage ปรากฏบนสแตกตามที่เป็น หากดูโปรเซสที่มีชีวิต รายการเธรดและมุมมองสแตกของ Process Explorer มีประโยชน์ หากจะตามตามเวลา การจับเทรซ WPR ได้ผล ดูบทความแนะนำ WinDbg ของไซต์นี้ Process Explorer ในทางปฏิบัติ และ WPR/WPA ในทางปฏิบัติด้วย