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

· · 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

โครงสร้างพื้นฐานของลูปข้อความOS วางเมาส์ แป้นพิมพ์ และอินพุตอื่นเข้าสู่คิวข้อความของเธรด ลูปของเธรด UI ดึงด้วย GetMessage เรียก window procedure ด้วย DispatchMessage และกลับสู่ยอดลูปเมื่อการประมวลผลเสร็จOS (อินพุต คำขอวาดใหม่ ตัวตั้งเวลา)คิวข้อความของเธรดดึงด้วย GetMessageDispatchMessageจัดการใน window procedure

ภาพ 1: หัวใจของแอป GUI คือลูปข้อความ ตัวจัดการอีเวนต์ทุกตัวรันเป็นหนึ่งรอบของลูปนี้

โครงสร้างนี้มีผลสำคัญอย่างหนึ่ง หากคุณทำงานที่ใช้เวลานานภายใน window procedure (ตัวจัดการอีเวนต์) ลูปดึงข้อความถัดไปไม่ได้ในระหว่างนั้น มันตอบสนองทั้งต่อคลิกและต่อคำขอวาดใหม่ไม่ได้ — นั่นคือสิ่งที่ “แฮง” เป็นจริง ๆ

ยังคุ้มที่จะรับไว้ว่าข้อความถูกส่งด้วยสองเส้นทาง PostMessage วางข้อความบนคิวแล้วกลับทันที และลูปดึงแล้วประมวลผลข้อความตามลำดับ SendMessage ในทางกลับกัน เรียก window procedure โดยตรงและไม่กลับสู่ผู้เรียกจนกว่าการประมวลผลเสร็จ36 ความต่างนั้นพาตรงเข้าสู่การอภิปรายเดดล็อกในบทที่ 4

สองเส้นทางการส่งข้อความPostMessage วางข้อความบนคิวแล้วกลับทันที ลูปข้อความดึงแล้วประมวลผลตามลำดับ SendMessage เรียก procedure โดยตรงและไม่กลับสู่ผู้เรียกจนกว่าการประมวลผลเสร็จPostMessageวางบนคิว (กลับทันที)ลูปดึงแล้วประมวลผลตามลำดับSendMessageเรียก procedure โดยตรงไม่กลับจนกว่าการประมวลผลเสร็จ

ภาพ 2: แม้ทั้งคู่ “ส่งข้อความ” Post ที่เข้าคิวกับ Send ที่รอความเสร็จมีธรรมชาติต่างกันโดยสิ้นเชิง

3. “ไม่ตอบสนอง” ถูกตัดสินอย่างไร — กฎ 5 วินาทีกับหน้าต่างโกสต์

แล้ว OS รู้ได้อย่างไรว่า “แอปนี้แฮง” เกณฑ์ถูกบันทึกอย่างเป็นทางการ OS ถือว่าหน้าต่างไม่ตอบสนองเมื่อมันไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้เรียก PeekMessage (การดึงข้อความ) เป็นเวลา 5 วินาที1 กล่าวอีกนัย OS เฝ้าว่า “ลูปข้อความกำลังหมุนจริงหรือไม่” แบบที่คุณจับชีพจร และหากไม่มีชีพจรเป็นเวลา 5 วินาทีมันตัดสินว่าหน้าต่างไม่ตอบสนอง (เอกสารระบุว่าค่า 5 วินาทีนี้อาจเปลี่ยนในอนาคต) หน่วยของการตัดสินคือหน้าต่างและเธรด GUI ที่เป็นเจ้าของ ในแอปที่มีหลายเธรด UI การแฮงของเธรดหนึ่งไม่หมายความว่าหน้าต่างบนอีกเธรดตาย เธรดที่ต้องดูในดัมป์คือเจ้าของหน้าต่างที่แฮง

สิ่งที่เกิดกับหน้าต่างระดับบนที่ถูกตัดสินก็ถูกบันทึกเช่นกัน OS ซ่อนหน้าต่างเดิมแล้วแทนที่ด้วย “หน้าต่างโกสต์” ที่มีลำดับ Z ตำแหน่ง ขนาด และรูปลักษณ์เดียวกัน สิ่งที่ผู้ใช้ทำได้คือย้าย ปรับขนาด หรือปิด (บังคับ) แอปข้างในไม่ได้ตอบสนองจริง ดังนั้นการทำงานอื่นใช้ไม่ได้2

การตัดสินหน้าต่างแฮงและการสลับหน้าต่างโกสต์เมื่อเธรด UI ถูกบล็อกบนกิจหนักและการดึงข้อความหยุดเป็นเวลา 5 วินาที OS ตัดสินว่าหน้าต่างไม่ตอบสนอง ซ่อนของเดิม สลับหน้าต่างโกสต์รูปลักษณ์เดียวกันเข้ามา และเสนอให้ผู้ใช้เฉพาะย้าย ย่อ และปิดไม่ใช่เธรด UI ถูกบล็อกบนกิจหนักการดึงข้อความหยุดผ่าน 5 วินาที?สลับหน้าต่างโกสต์เข้ามาชื่อแสดง (ไม่ตอบสนอง)ขาวเหมือนน้ำค้างแข็ง ย้ายและปิดได้อย่างเดียว

ภาพ 3: ทั้งข้อความ “ไม่ตอบสนอง” และหน้าจอขาวเป็นของหน้าต่างโกสต์ที่ OS สลับเข้ามา ไม่ใช่ของแอปที่แฮง

สตริง “(ไม่ตอบสนอง)” ที่ปรากฏบนแถบชื่อ และลักษณะขาวเหมือนน้ำค้างแข็งภายใต้ธีม Aero ทั้งคู่เป็นของหน้าต่างโกสต์นี้ ผลเชิงปฏิบัติสองอย่างตามมา

  • เมื่อถึงเวลาที่ “ไม่ตอบสนอง” ถูกแสดง เธรดที่เป็นเจ้าของหน้าต่างนั้นไม่ได้ประมวลผลข้อความอย่างน้อย 5 วินาที ไม่ใช่ว่า “การแสดงมาเร็วเกินไป” — เธรด UI ถูกบล็อกแน่นอน
  • หน้าต่างโกสต์ไม่ถูกสร้างขณะมีดีบักเกอร์ติดอยู่2 เมื่อดูราวกับว่า “มันไม่เคยไปไม่ตอบสนองภายใต้ดีบักเกอร์ แต่ไปในรีลีส” แฮงเองอาจเหมือนกันและมีเพียงการแสดงที่ต่าง

ยังมี API DisableProcessWindowsGhosting ที่ปิดการสลับนี้สำหรับทั้งโปรเซส7 มันมีไว้สำหรับกรณีพิเศษ เช่น เทอร์มินัลคีออสก์ที่คุณไม่อยากให้ OS ทำให้หน้าต่างดูใช้งานได้ด้วยตนเอง การเรียกมันหยุดการแสดง “ไม่ตอบสนอง” ไม่ให้ปรากฏ แต่ความจริงที่แอปแฮงไม่เปลี่ยน ให้เข้าใจว่านี่ไม่ใช่สิ่งที่แอปทั่วไปใช้เป็นมาตรการ “ไม่ตอบสนอง”

4. ทำไมแอปจึงแฮง — รูปแบบคลาสสิกที่บล็อกเธรด UI

สาเหตุ เมื่อต้มลง คือจุดเดียว — “เธรด UI ไม่กลับสู่ลูปข้อความ” — แต่รูปที่คุณพบในทางปฏิบัติตกเป็นของคลาสสิกไม่กี่อย่าง

การจัดประเภทสาเหตุคลาสสิกที่บล็อกเธรด UIสี่ตระกูลคลาสสิก — I/O แบบซิงโครนัสและการเรียกเครือข่าย การรอล็อก SendMessage ข้ามเธรด และการเกี่ยวข้องของ COM STA — ทั้งหมดรวมเหลือจุดเดียวที่เธรด UI กลับสู่ลูปข้อความไม่ได้สาเหตุคลาสสิกใด?I/O หรือล็อก?SendMessage หรือ COM?I/O ซิงค์และเครือข่ายการรอล็อกSendMessageข้ามเธรดการเกี่ยวข้องของ COM STAUI กลับไม่ได้การตรวจไม่ตอบสนอง

ภาพ 4: อาการที่เห็นเหมือนกัน แต่ตัวการที่บล็อกเธรดตกเป็นสี่ตระกูล และมาตรการต่างกันสำหรับแต่ละอย่าง

I/O แบบซิงโครนัสและการเรียกเครือข่าย นี่พบบ่อยที่สุด รูปแบบของการทำแบบซิงโครนัส ภายในตัวจัดการคลิกปุ่ม การอ่านหรือเขียนไฟล์ใหญ่ คิวรีฐานข้อมูล การเรียก Web API หรือการเข้าถึงไฟล์บนไดรฟ์เครือข่าย บนเครื่องพัฒนามันเสร็จในเสี้ยววินาที จึงไม่สังเกต ความหน่วงเครือข่ายในโปรดักชันหรือสะดุดของไฟล์เซิร์ฟเวอร์เปลี่ยนเป็นการรอหลายสิบวินาที และคุณได้ตั๋วว่า “มันแฮงเป็นครั้งคราว” ไดรฟ์เครือข่ายมีหมดเวลานานเมื่อการเชื่อมต่อขาด และทำให้อาการแย่ลงอย่างมาก

การรอล็อก รูปแบบที่เธรด UI พยายามถือล็อกบนข้อมูลที่ใช้ร่วมกับเธรดเวิร์กเกอร์ แล้วลงเอยด้วยการรอเวิร์กเกอร์ที่ถือล็อกนั้นนาน วินัยล็อกมีรายละเอียดในซีรีส์มัลติเธรดเชิงปฏิบัติ

SendMessage ข้ามเธรด SendMessage ไม่กลับจนกว่า procedure ของหน้าต่างปลายทางประมวลผลเสร็จ6 เมื่อคุณส่งไปยังหน้าต่างบนอีกเธรด ผู้ส่งถูกทำให้รอจนกว่าเธรดนั้นอยู่ในสถานะที่ประมวลผลข้อความได้ หากเธรดปลายทางเองกำลังรออะไรอยู่ คุณได้เดดล็อกข้อความที่แต่ละฝั่งรออีกฝั่ง3 การส่งไปยัง HWND_BROADCAST โดยเฉพาะจะลากคุณเข้าทันทีที่หน้าต่างเดียวไม่ตอบสนอง เมื่อคุณรอไม่ได้ ให้พิจารณา SendMessageTimeout หรือ PostMessage ซึ่งไม่รอคำตอบ8

เดดล็อกที่เกิดจาก SendMessage ข้ามเธรดหากเธรดเวิร์กเกอร์ส่ง SendMessage ไปยังหน้าต่างของเธรด UI ขณะที่เธรด UI ถูกบล็อกรอผลของเวิร์กเกอร์ แต่ละฝั่งรอให้อีกฝั่งเสร็จแล้วคุณได้เดดล็อกเธรดเวิร์กเกอร์เธรด UIเธรดเวิร์กเกอร์เธรด UIประมวลผลข้อความไม่ได้ (ถูกบล็อก)กลับจาก SendMessage ไม่ได้รอกันและกัน — เดดล็อกรอให้เวิร์กเกอร์เสร็จ (ถูกบล็อก)SendMessage (ไม่กลับจนกว่าประมวลผล)

ภาพ 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

การแบ่งบทบาทในแอปที่ไม่แฮงเธรด UI รับผิดชอบเฉพาะการรับอินพุต การแสดงความคืบหน้า และการรับการยกเลิก เธรดเวิร์กเกอร์รันงานหนักแล้วคืนความเสร็จสู่เธรด UI ผ่าน PostMessage หรือความต่อเนื่องของ awaitส่งต่องานPostMessage / ความต่อเนื่องของ awaitเธรด UI: อินพุต ความคืบหน้า ยกเลิกเธรดเวิร์กเกอร์: งานหนักไม่มี I/O ซิงค์หรือการคำนวณยาวบนเธรด UI

ภาพ 6: คงเธรด UI เป็น “โต๊ะรับ” ส่งงานหนักให้เวิร์กเกอร์เสมอ และรับเฉพาะการแจ้งความเสร็จ

สิ่งที่อยากหลีกเลี่ยงคือเทคนิคของการแทรก Application.DoEvents() หรือลูป PeekMessage ระหว่างชิ้นงานหนักเพียงเพื่อให้การแสดงมีชีวิต คุณหลบไม่ตอบสนอง แต่ตัวจัดการอีเวนต์ใด ๆ เข้าซ้ำ กลางงาน การคลิกปุ่มครั้งที่สอง การปิดฟอร์มระหว่างประมวลผล ตัวตั้งเวลายิง — อย่างใดอย่างหนึ่งสามารถทำให้ข้อมูลที่ยังกำลังประมวลผลเสีย และบั๊กขึ้นกับจังหวะและทำซ้ำยาก คงการปั๊มลูปข้อความด้วยมือภายในโครงสร้างจำกัด เช่น กล่องโต้ตอบความคืบหน้าแบบโมดัล และตามกฎแก้ด้วยการแยก

ไทม์ไลน์ของบั๊กการเข้าซ้ำที่เกิดจาก DoEventsการเรียก DoEvents กลางงานหนักให้อีเวนต์คลิกที่อยู่ในคิวขัดและรัน เขียนทับข้อมูลที่ยังกำลังประมวลผล แล้วกลับไปงานเดิม ก่อการเสียข้อมูลที่ขึ้นกับจังหวะเธรด UIเธรด UIตัวจัดการคลิกปุ่มซ้ำขัดเข้ามางานหนักเริ่ม (ข้อมูลกำลังประมวลผล)DoEvents (ประมวลผลข้อความในคิว)งานที่ขัดเขียนทับข้อมูลงานเดิมกลับมา (ข้อมูลไม่สอดคล้องแล้ว)

ภาพ 7: DoEvents ลบ “ไม่ตอบสนอง” แลกกับการเชิญอีเวนต์ใด ๆ เข้ากลางงาน

สำหรับงานที่รันนาน ให้รวมการแสดงความคืบหน้าและการยกเลิกในการออกแบบด้วย ส่งความคืบหน้าไปยัง UI ด้วย IProgress<T> และสื่อสารการขัดจังหวะด้วย CancellationToken แล้วผู้ใช้เห็นว่า “มันกำลังทำงาน” และจะไม่เอื้อมไปยุติบังคับ (ซึ่งมักเป็นสาเหตุของการเสียข้อมูล)

ลำดับความคืบหน้าและการยกเลิกสำหรับงานที่รันนานเธรดเวิร์กเกอร์ส่งความคืบหน้าไปยังเธรด UI ผ่าน IProgress การยกเลิกบน UI ถึงเวิร์กเกอร์ผ่าน CancellationToken เวิร์กเกอร์หยุดที่ขอบที่สะดวกแล้วทำความสะอาดความคืบหน้าผ่าน IProgressCancellationTokenเวิร์กเกอร์: งานที่รันนานUI: ความคืบหน้าและปุ่มหยุดหยุดที่ขอบแล้วทำความสะอาด

ภาพ 8: ความคืบหน้าคือ “เวิร์กเกอร์ → UI” การยกเลิกคือ “UI → เวิร์กเกอร์” รวมช่องสองทางบางนี้ในการออกแบบตั้งแต่ต้น

6. สอบสวนชั่วขณะที่แฮง

ในการสอบสวน “มันแฮงเป็นครั้งคราว” สิ่งที่มีค่าที่สุดคือสถานะเธรดที่ชั่วขณะพอดีที่มันแฮง รีบูต แล้วหลักฐานหาย

ถ่ายดัมป์ บนแท็บรายละเอียดของตัวจัดการงาน คลิกขวาโปรเซสเป้าหมาย → “สร้างไฟล์ดัมป์” แค่นั้นให้ดัมป์เต็มพร้อมสแตกของทุกเธรด การบอกเจ้าหน้าที่ไอทีที่รับตั๋วว่า “เมื่อมันแฮง ให้ถ่ายสิ่งนี้ก่อนปิด” เปลี่ยนอัตราความสำเร็จของการสอบสวนมาก สำหรับการสร้างกลไกเก็บ ดูบทความเรื่องการเก็บ crash dump

ดูสแตกของเธรด UI เปิดดัมป์ใน WinDbg แล้วดูสแตกของเธรดที่กำลังปั๊มลูปข้อความ (มักเป็นเธรด 0) I/O แบบซิงโครนัสปรากฏเป็น ReadFile หรือ API เครือข่าย การรอล็อกเป็นเรียกตระกูล WaitFor… และ SendMessage ข้ามเธรดเป็นการรอภายใน SendMessage — ตามที่เป็น วิธีอ่านอธิบายในบทความแนะนำ WinDbg

ดูแบบมีชีวิต ด้วย Process Explorer คุณตรวจรายการเธรดและสแตกได้ทันที เมื่อมันเชื่องช้าอย่างคงที่ ให้ถ่ายเทรซ WPR แล้ววิเคราะห์การรอของเธรด UI ตามเวลา (WPR/WPA ในทางปฏิบัติ)

ขั้นตอนพื้นฐานสำหรับการสอบสวนไม่ตอบสนองถ่ายดัมป์ที่ชั่วขณะที่แฮง ดูสแตกของเธรด UI ระบุว่าหยุดบน I/O แบบซิงโครนัส การรอล็อก หรือ SendMessage ข้ามเธรด แล้วเชื่อมโยงกับการแก้การออกแบบที่ตรงกันชั่วขณะที่แฮงถ่ายดัมป์ (ก่อนปิด)ดูสแตกของเธรด UII/O ซิงค์หรือการรอเครือข่ายการรอล็อกSendMessage ข้ามเธรดแยกจุดไปเวิร์กเกอร์

ภาพ 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 วินาที” ทำงานย้อนจากประโยคนั้น ผู้สมัครสาเหตุ การแก้ และขั้นตอนสอบสวนทั้งหมดหลุดออกมาตามธรรมชาติ

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

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

KomuraSoft LLC รับการสอบสวนหาสาเหตุของแอปธุรกิจที่ “แฮงเป็นครั้งคราว” หรือไป “ไม่ตอบสนอง” (การวิเคราะห์ดัมป์และการวิเคราะห์เทรซ) การรีแฟกเตอร์โค้ด UI เดิมที่เต็มไปด้วยงานซิงโครนัสไปสู่ async/await และการแยกเธรดเวิร์กเกอร์ และการตรวจทานการออกแบบ UI ที่ไม่แข็ง แม้เมื่อคุณยังไม่มีขั้นตอนทำซ้ำ เราช่วยได้ตั้งแต่การออกแบบวิธีเก็บหลักฐาน

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

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). ว่าด้วยเกณฑ์การตัดสินที่แอปถูกถือว่าไม่ตอบสนองเมื่อมัน “ไม่ได้รออินพุต ไม่อยู่ในลำดับเริ่มต้น และไม่ได้เรียก PeekMessage เป็นเวลาหมดเวลาภายใน 5 วินาที” ว่าด้วยเกณฑ์ 5 วินาทีนี้อาจเปลี่ยน และว่าด้วยฟังก์ชันคืน TRUE เสมอสำหรับหน้าต่างโกสต์  2

  2. Microsoft Learn, GetMessage function (winuser.h). ว่าด้วยระบบถือว่าหน้าต่างระดับบนไม่ตอบสนองเมื่อมันหยุดตอบสนองต่อข้อความหลายวินาทีแล้วแทนที่ด้วยหน้าต่างโกสต์ที่มีลำดับ Z ตำแหน่ง ขนาด และรูปลักษณ์เดียวกัน ว่าด้วยผู้ใช้ย้าย ปรับขนาด หรือปิดได้อย่างเดียว และว่าด้วยหน้าต่างโกสต์ไม่ถูกสร้างขณะมีดีบักเกอร์ติดอยู่  2 3

  3. Microsoft Learn, About Messages and Message Queues. ว่าด้วยแอป Windows เป็นแบบขับด้วยอีเวนต์และ window procedure ประมวลผลข้อความ ว่าด้วยความต่างระหว่างข้อความในคิวกับข้อความที่ส่งโดยตรง ว่าด้วยการสลับหน้าต่างที่ไม่ตอบสนองเป็นหน้าต่างโกสต์ และว่าด้วยตอนที่ครอบเดดล็อกจากเธรดที่ส่งข้อความหากัน  2 3 4

  4. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). ว่าด้วยคอนโทรล WinForms ไม่ปลอดภัยที่จะแตะจากเธรดอื่นนอกจากเธรดที่สร้างพวกมัน ว่าด้วยการใช้ Invoke/BeginInvoke สำหรับการอัปเดตจากอีกเธรด และว่าด้วยรูปแบบอะซิงโครนัสที่ปลอดภัยโดยใช้ async/await หรือ BackgroundWorker  2

  5. Microsoft Learn, Using Messages and Message Queues. ว่าด้วยการอิมพลีเมนต์ลูปข้อความทั่วไปด้วย GetMessage, TranslateMessage และ DispatchMessage และวิธีตรวจคิวข้อความ 

  6. Microsoft Learn, SendMessage function (winuser.h). ว่าด้วย SendMessage เรียก window procedure ของหน้าต่างที่ระบุและไม่กลับจนกว่าการประมวลผลเสร็จ ว่าด้วยการส่งไปยังหน้าต่างบนอีกเธรดทำให้ผู้ส่งรอจนกว่าเธรดนั้นประมวลผลข้อความ และว่าด้วยความต่างจาก PostMessage ซึ่งวางข้อความบนคิวโดยไม่รอคำตอบ  2 3

  7. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). ว่าด้วยสามารถปิด สำหรับโปรเซส GUI ที่เรียก ฟีเจอร์หน้าต่างโกสต์ที่ทำให้หน้าต่างที่ไม่ตอบสนองย่อ ย้าย และปิดได้ และว่าด้วยการปิดมีอยู่ตลอดอายุโปรเซส 

  8. 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 แล้วการจัดรูปแบบพัง ปิดแอปต้นทางแล้ววางไม่ได้อีก — ทั้งคู่มาจากคลิปบอร์ดที่วางเนื้อหาเดียวกันในหลายรูปแบบพร้อมกัน บทความนี...

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

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

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

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

ภายใต้เงื่อนไขใด "ไม่ตอบสนอง" จึงปรากฏ
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 ในทางปฏิบัติด้วย

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

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

Go Komura

ผู้แทนของ KomuraSoft LLC

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

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

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