"Không phản hồi" thực sự là gì — Cách Windows quyết định ứng dụng đã treo, và cách thiết kế ứng dụng không treo

· · Windows, Phát triển Windows, Xử lý sự cố, Đa luồng, WinForms, WPF, Win32 API, Thiết kế UI

“Ứng dụng hóa trắng giữa thao tác và hiện (Không phản hồi).” “Chúng tôi nhận phiếu rằng nó treo thỉnh thoảng, nhưng không bao giờ tái hiện trên máy phát triển.” — Với ứng dụng nghiệp vụ Windows, “Không phản hồi” này là một trong những khiếu nại phổ biến nhất. Và điều ít được biết đến một cách đáng ngạc nhiên là thứ đặt lên hiển thị “Không phản hồi” không phải bản thân ứng dụng đã treo — đó là hệ điều hành.

Windows biết ứng dụng đã “treo” bằng cách nào? Cửa sổ trắng mờ đó là gì? Hướng tới các nhà phát triển viết ứng dụng nghiệp vụ trên Windows và nhân viên IT nhận phiếu về ứng dụng treo, bài viết này lần theo phán đoán “Không phản hồi” từ nền tảng của vòng thông điệp, và sắp xếp các nguyên nhân treo kinh điển, thiết kế không treo, và thủ tục điều tra khoảnh khắc treo — tất cả dựa trên nguồn gốc.

1. Kết luận trước

  • “Không phản hồi” là phán đoán của hệ điều hành. Khi một cửa sổ (và luồng GUI sở hữu nó) không đang chờ đầu vào, không đang trong chuỗi khởi động, và chưa lấy thông điệp (PeekMessage) trong 5 giây, hệ điều hành coi nó không phản hồi. Phán đoán không phải theo từng tiến trình.1
  • Cửa sổ hóa trắng là “cửa sổ ma”. Hệ điều hành đã ẩn cửa sổ gốc và đổi vào một bản giả cùng vị trí, kích thước và hình dạng. Tất cả bạn có thể làm là di chuyển, thu nhỏ, hoặc đóng; nội dung không đang chạy. Cửa sổ ma không được tạo khi debugger đang gắn.2
  • Nguyên nhân treo gần như luôn quy về một điều. Luồng UI lẽ ra phải bơm vòng thông điệp bị chặn trên việc nặng hoặc lần chờ. I/O đồng bộ, lời gọi mạng, chờ khóa, và SendMessage xuyên luồng là các mẫu kinh điển.3
  • Nguyên tắc thiết kế là “đừng chờ hoặc tính trên luồng UI”. Chuyển việc nặng sang luồng worker (async/await + Task.Run trong C#) và để luồng UI chuyên vẽ, tiến độ, và nhận hủy.4
  • DoEvents và bơm vòng thông điệp thủ công là ổ nuôi lỗi tái nhập. Hiển thị “Không phản hồi” biến mất, nhưng cấu trúc giờ cho phép sự kiện tùy ý cắt giữa việc. Tách, không trốn, là cách đúng.
  • Điều tra bắt đầu bằng nắm trạng thái đúng khoảnh khắc treo. Lấy dump và nhìn stack của luồng UI, và bạn gần như luôn nhận diện nó đang chờ gì.

2. Điều kiện tiên quyết: Ứng dụng Windows được dẫn bởi thông điệp

Để hiểu “Không phản hồi”, trước hết bạn cần tiếp nhận rằng ứng dụng GUI Windows là dẫn bởi sự kiện. Ứng dụng GUI không tự đi lấy đầu vào; nó nhận thông điệp hệ điều hành gửi (chuột, bàn phím, yêu cầu vẽ lại, bộ hẹn giờ, và tương tự) và hành động theo chúng.3

Mỗi luồng tạo cửa sổ có một hàng đợi thông điệp và chạy một vòng thông điệp như thế này.

MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);   // the window procedure is called
}

GetMessage lấy thông điệp từ hàng đợi, và DispatchMessage gọi thủ tục cửa sổ của cửa sổ đó (hàm xử lý thông điệp). Xử lý nhấn nút, vẽ lại, và handler sự kiện WinForms hoặc WPF, khi bạn đun chúng xuống, đều chạy bên trong một lần lặp của vòng này.5

Cấu trúc cơ bản của vòng thông điệpHệ điều hành đặt chuột, bàn phím và đầu vào khác vào hàng đợi thông điệp của luồng; vòng của luồng UI lấy nó bằng GetMessage, gọi thủ tục cửa sổ bằng DispatchMessage, và trở về đầu vòng khi xử lý xongOS(đầu vào, yêu cầu vẽ lại, bộ hẹn giờ)Hàng đợi thông điệp của luồngLấy bằng GetMessageDispatchMessageXử lý trong thủ tục cửa sổ

Hình 1: Trái tim của ứng dụng GUI là vòng thông điệp; mọi handler sự kiện chạy như một lần lặp của vòng này.

Cấu trúc này có một hệ quả quan trọng. Nếu bạn làm việc tốn thời gian bên trong thủ tục cửa sổ (một handler sự kiện), vòng không thể lấy thông điệp tiếp trong lúc đó. Nó không thể phản ứng với nhấn lẫn với yêu cầu vẽ lại — đó mới thật sự là “treo”.

Cũng đáng tiếp nhận rằng thông điệp được gửi theo hai đường. PostMessage đặt thông điệp lên hàng đợi rồi trở về ngay, và vòng lấy rồi xử lý thông điệp theo thứ tự. SendMessage, mặt khác, gọi thủ tục cửa sổ trực tiếp và không trở về phía gọi cho tới khi xử lý hoàn tất.36 Sự khác đó đi thẳng vào thảo luận deadlock ở Chương 4.

Hai đường gửi thông điệpPostMessage đặt thông điệp lên hàng đợi rồi trở về ngay; vòng thông điệp lấy và xử lý thông điệp theo thứ tự. SendMessage gọi thủ tục trực tiếp và không trở về phía gọi cho tới khi xử lý hoàn tấtPostMessageĐặt lên hàng đợi(trở về ngay)Vòng lấy và xử lý theo thứ tựSendMessageGọi thủ tục trực tiếpKhông trở về cho tới khi xử lý xong

Hình 2: Dù cả hai đều “gửi thông điệp”, một Post xếp hàng và một Send chờ hoàn tất có bản chất hoàn toàn khác.

3. Cách “Không phản hồi” được phán — Quy tắc 5 giây và cửa sổ ma

Vậy hệ điều hành biết “ứng dụng này đã treo” bằng cách nào? Tiêu chí được ghi chính thức. Hệ điều hành coi một cửa sổ không phản hồi khi nó không đang chờ đầu vào, không đang trong chuỗi khởi động, và chưa gọi PeekMessage (lấy thông điệp) trong 5 giây.1 Nói cách khác, hệ điều hành theo dõi liệu “vòng thông điệp có thực sự đang quay” như bạn bắt mạch, và nếu không có mạch trong 5 giây nó phán cửa sổ không phản hồi (tài liệu nêu rằng giá trị 5 giây này có thể đổi trong tương lai). Đơn vị phán đoán là cửa sổ và luồng GUI sở hữu nó; trong ứng dụng có vài luồng UI, một luồng treo không nghĩa là cửa sổ trên luồng khác đã chết. Luồng cần nhìn trong dump là chủ sở hữu của cửa sổ đã treo.

Điều xảy ra với cửa sổ cấp cao nhất đã bị phán cũng được ghi. Hệ điều hành ẩn cửa sổ gốc và thay nó bằng “cửa sổ ma” có cùng thứ tự Z, vị trí, kích thước và hình dạng. Tất cả người dùng có thể làm với nó là di chuyển, đổi kích thước, hoặc (cưỡng bức) đóng. Ứng dụng bên trong thực ra không đang phản hồi, nên không thao tác nào khác hoạt động.2

Phán đoán cửa sổ treo và việc đổi cửa sổ maKhi luồng UI bị chặn trên việc nặng và việc lấy thông điệp dừng trong 5 giây, hệ điều hành phán cửa sổ không phản hồi, ẩn bản gốc, đổi vào cửa sổ ma cùng hình dạng, và chỉ cho người dùng di chuyển, thu nhỏ, và đóngchưarồiLuồng UI bị chặn trên việc nặngViệc lấy thông điệp dừngĐã 5 giây?Đổi vào cửa sổ maTiêu đề hiện(Không phản hồi)Trắng mờ; chỉ di chuyển và đóng

Hình 3: Cả chữ “Không phản hồi” lẫn màn trắng đều thuộc về cửa sổ ma hệ điều hành đã đổi vào, không thuộc ứng dụng đã treo.

Chuỗi “(Không phản hồi)” xuất hiện trên thanh tiêu đề, và vẻ trắng mờ dưới chủ đề Aero, đều thuộc về cửa sổ ma này. Hai hệ quả thực tiễn theo sau.

  • Tới lúc “Không phản hồi” được hiển thị, luồng sở hữu cửa sổ đó đã không xử lý thông điệp ít nhất 5 giây. Không phải “hiển thị lên quá sớm” — luồng UI chắc chắn đang bị chặn.
  • Cửa sổ ma không được tạo khi debugger đang gắn.2 Khi trông như “nó không bao giờ thành Không phản hồi dưới debugger, nhưng lại thành trong bản phát hành”, bản thân lần treo có thể giống nhau và chỉ hiển thị khác.

Cũng có API, DisableProcessWindowsGhosting, tắt việc đổi này cho cả tiến trình.7 Nó dành cho trường hợp đặc biệt như thiết bị kiosk nơi bạn không muốn hệ điều hành tự làm cửa sổ trông như thao tác được. Gọi nó thì hiển thị “Không phản hồi” ngừng xuất hiện, nhưng sự thật ứng dụng đang treo không đổi. Hãy hiểu rằng đây không phải thứ ứng dụng thông thường dùng như đối sách “Không phản hồi”.

4. Vì sao ứng dụng treo — Các mẫu kinh điển chặn luồng UI

Nguyên nhân, đun xuống, là một điểm duy nhất — “luồng UI không trở về vòng thông điệp” — nhưng các hình dạng bạn gặp trong thực tiễn rơi vào vài mẫu kinh điển.

Phân loại các nguyên nhân kinh điển chặn luồng UIBốn họ kinh điển — I/O đồng bộ và lời gọi mạng, chờ khóa, SendMessage xuyên luồng, và dính COM STA — đều quy về cùng một điểm duy nhất rằng luồng UI không thể trở về vòng thông điệpNguyên nhân kinh điển nào?I/O hay khóa?SendMessage hay COM?I/O đồng bộ và mạngChờ khóaSendMessagexuyên luồngDính COM STAUI không thể trở vềKiểm tra Không phản hồi

Hình 4: Triệu chứng nhìn thấy giống nhau, nhưng thủ phạm chặn luồng rơi vào bốn họ, và đối sách khác nhau cho mỗi họ.

I/O đồng bộ và lời gọi mạng. Đây là thứ phổ biến nhất. Mẫu làm đồng bộ, bên trong handler nhấn nút, lần đọc hoặc ghi tệp lớn, truy vấn cơ sở dữ liệu, lời gọi Web API, hoặc truy cập tệp trên ổ mạng. Trên máy phát triển nó xong trong một phần giây, nên bạn không bao giờ nhận ra; độ trễ mạng sản xuất hoặc ổ tệp máy chủ khựng biến nó thành lần chờ vài chục giây, và bạn nhận phiếu “nó treo thỉnh thoảng”. Ổ mạng có thời gian chờ dài khi kết nối đứt, và chúng làm triệu chứng tệ đi rõ rệt.

Chờ khóa. Mẫu trong đó luồng UI cố lấy khóa trên dữ liệu dùng chung với luồng worker, và cuối cùng chờ một worker giữ khóa đó lâu. Kỷ luật khóa được trình bày chi tiết trong loạt bài đa luồng thực tiễn.

SendMessage xuyên luồng. SendMessage không trở về cho tới khi thủ tục của cửa sổ đích đã xử lý xong.6 Khi bạn gửi nó tới cửa sổ trên luồng khác, bên gửi bị bắt chờ cho tới khi luồng đó ở trạng thái có thể xử lý thông điệp. Nếu luồng đích tự đang chờ thứ gì, bạn có deadlock thông điệp trong đó mỗi phía chờ phía kia.3 Gửi tới HWND_BROADCAST nói riêng sẽ kéo bạn vào ngay khi một cửa sổ đơn không phản hồi. Khi bạn không thể chịu chờ, cân nhắc SendMessageTimeout hoặc PostMessage, thứ không chờ trả lời.8

Deadlock do SendMessage xuyên luồngNếu luồng worker gửi SendMessage tới cửa sổ luồng UI trong khi luồng UI bị chặn chờ kết quả của worker, mỗi phía chờ phía kia xong và bạn có deadlockLuồng workerLuồng UILuồng workerLuồng UIKhông thể xử lý thông điệp(bị chặn)Không thể trở về từ SendMessageChờ nhau — deadlockĐang chờ worker xong(bị chặn)SendMessage(không trở về cho tới khi được xử lý)

Hình 5: “Luồng UI chờ worker, và worker chờ luồng UI qua SendMessage” là deadlock kinh điển.

Dính apartment COM. Lời gọi tới đối tượng STA được gửi như thông điệp cửa sổ, nên khi luồng UI (STA) bị chặn, lời gọi COM từ luồng khác cũng bị chặn như thiệt hại kèm. Cấu trúc đó được giải thích trong bài viết COM STA/MTA.

Đống “chỉ là một khoảnh khắc”. Ngay cả lời gọi đồng bộ 50 ms, được gọi 100 lần trong vòng, cũng là 5 giây. Ngưỡng Không phản hồi là 5 giây, nhưng “ì ạch” cảm nhận được bắt đầu quanh 100 ms. Quy tắc ngón tay cái của thiết kế là “luồng UI chỉ được chặn trong mili giây”.

5. Thiết kế không treo — Chuyển việc nặng khỏi luồng UI

Nguyên tắc thiết kế là một điều: chuyển việc tốn thời gian khỏi luồng UI. Trong C# (WinForms/WPF), async/await là cách đúng ngắn nhất.

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;
    }
}

Có ba điểm. Thứ nhất, trong lúc await đang chờ, luồng UI đã trở về vòng thông điệp, nên bạn không thành Không phản hồi. Thứ hai, phần tiếp sau await trở về luồng UI, nên bạn có thể chạm điều khiển bình thường sau đó (chạm điều khiển trực tiếp từ luồng worker bị cấm; nếu cần, dùng Control.Invoke / Dispatcher.InvokeAsync).4 Thứ ba, tắt nút trong lúc việc đang chạy, và nếu không thì giết tái nhập bằng thiết kế.

Bức tranh giống nhau trong Win32 native: giao việc cho luồng worker, thông báo luồng UI về hoàn tất như thông điệp tùy chỉnh qua PostMessage, và cập nhật UI trong thủ tục cửa sổ. PostMessage chỉ đặt thông điệp lên hàng đợi rồi trở về ngay, nên phía worker cũng không bị chặn.6 Viết bản thân lần chờ luồng worker với kỷ luật được trình bày trong bài viết về biến điều kiện.

Phân vai trong ứng dụng không treoLuồng UI chỉ chịu trách nhiệm nhận đầu vào, hiện tiến độ, và nhận hủy; luồng worker chạy việc nặng và trả hoàn tất về luồng UI qua PostMessage hoặc phần tiếp awaitGiao việcPostMessage / phần tiếp awaitLuồng UI: đầu vào, tiến độ, hủyLuồng worker: việc nặngKhông I/O đồng bộ hay tính lâu trên luồng UI

Hình 6: Giữ luồng UI như “quầy tiếp nhận”, luôn giao việc nặng cho worker, và chỉ lấy thông báo hoàn tất.

Thứ bạn muốn tránh là kỹ thuật chèn Application.DoEvents() hoặc vòng PeekMessage giữa các khối việc nặng chỉ để giữ hiển thị sống. Bạn tránh Không phản hồi, nhưng handler sự kiện tùy ý tái nhập giữa việc. Lần nhấn nút thứ hai, đóng form giữa xử lý, bộ hẹn giờ kích — bất kỳ cái nào có thể làm hỏng dữ liệu vẫn đang được xử lý, và lỗi phụ thuộc thời điểm và khó tái hiện. Giữ bơm vòng thông điệp thủ công bên trong cấu trúc hạn chế như hộp thoại tiến độ modal, và theo quy tắc hãy giải bằng tách.

Dòng thời gian của lỗi tái nhập do DoEventsGọi DoEvents giữa việc nặng cho phép handler sự kiện của lần nhấn đã xếp hàng cắt vào và chạy, viết lại dữ liệu vẫn đang được xử lý, rồi tiếp tục việc gốc, tạo hỏng dữ liệu phụ thuộc thời điểmLuồng UILuồng UIHandler nhấn lại nút cắt vàoViệc nặng bắt đầu(dữ liệu đang được xử lý)DoEvents(xử lý thông điệp đã xếp hàng)Việc cắt vào viết lại dữ liệuViệc gốc tiếp tục(dữ liệu đã không nhất quán)

Hình 7: DoEvents xóa “Không phản hồi” để đổi lấy việc mời sự kiện tùy ý vào giữa việc.

Với việc chạy lâu, hãy gồm cả hiển thị tiến độ và hủy vào thiết kế. Gửi tiến độ tới UI bằng IProgress<T> và truyền gián đoạn bằng CancellationToken, và người dùng có thể thấy rằng “nó đang làm việc” và sẽ không với tới chấm dứt cưỡng bức (thứ thường là nguyên nhân hỏng dữ liệu).

Luồng tiến độ và hủy cho việc chạy lâuLuồng worker gửi tiến độ tới luồng UI qua IProgress; hành động hủy trên UI tới worker qua CancellationToken; worker dừng ở ranh giới thuận tiện rồi dọnTiến độ qua IProgressCancellationTokenWorker: việc chạy lâuUI: tiến độ và nút DừngDừng ở ranh giới rồi dọn

Hình 8: Tiến độ là “worker → UI”; hủy là “UI → worker”. Gồm kênh hai chiều mỏng này vào thiết kế từ đầu.

6. Điều tra khoảnh khắc treo

Trong điều tra “nó treo thỉnh thoảng”, thứ quý nhất là trạng thái luồng đúng khoảnh khắc nó đang treo. Khởi động lại, và bằng chứng biến mất.

Lấy dump. Trên tab Details của Task Manager, nhấp phải tiến trình đích → “Create dump file”. Chỉ thế đã cho bạn dump đầy đủ với stack của mọi luồng. Chỉ cần bảo nhân viên IT nhận phiếu “khi nó treo, lấy cái này trước khi đóng” đã đổi tỷ lệ thành công của điều tra rất nhiều. Để xây cơ chế thu thập, xem bài viết về thu thập crash dump.

Nhìn stack của luồng UI. Mở dump trong WinDbg và nhìn stack của luồng đang bơm vòng thông điệp (thường là luồng 0). I/O đồng bộ hiện như ReadFile hoặc API mạng, chờ khóa như lời gọi họ WaitFor…, và SendMessage xuyên luồng như chờ bên trong SendMessage — nguyên như vậy. Cách đọc được giải thích trong bài nhập môn WinDbg.

Nhìn nó sống. Với Process Explorer bạn có thể kiểm tra danh sách luồng và stack tại chỗ. Khi nó ì ạch đều, lấy vết WPR và phân tích các lần chờ của luồng UI theo thời gian (WPR/WPA trong thực tiễn).

Thủ tục cơ bản điều tra Không phản hồiLấy dump đúng khoảnh khắc treo, nhìn stack của luồng UI, nhận diện liệu nó dừng trên I/O đồng bộ, chờ khóa, hay SendMessage xuyên luồng, và nối điều đó với sửa thiết kế khớpKhoảnh khắc treoLấy dump(trước khi đóng)Nhìn stack của luồng UII/O đồng bộ hoặc chờ mạngChờ khóaSendMessage xuyên luồngTách chỗ đó sang worker

Hình 9: Ngôi sao của điều tra là “dump của khoảnh khắc treo”; stack của luồng UI tự nó là phân loại nguyên nhân.

Bạn cũng có thể chuẩn hóa cách lấy lần cắt đầu từ triệu chứng. Nếu nó luôn treo trên một thao tác cụ thể, trước hết nghi I/O đồng bộ bên trong handler đó. Nếu nó treo hiếm và không tương quan với thao tác, nghi thứ tự khóa hoặc deadlock SendMessage xuyên luồng, và khớp mục tiêu chờ của cả hai luồng trong dump. Nếu nó chỉ treo trong một môi trường cụ thể, nghi hết thời gian chờ từ yếu tố môi trường như ổ mạng, proxy, hoặc phần mềm diệt virus.

7. Tóm tắt

  • “Không phản hồi” là cơ chế trong đó hệ điều hành phán rằng ứng dụng chưa lấy thông điệp trong 5 giây và đổi vào cửa sổ ma. Thứ đặt lên hiển thị là hệ điều hành, không phải ứng dụng.
  • Nguyên nhân treo là một điểm duy nhất: “luồng UI không thể trở về vòng thông điệp”. I/O đồng bộ, mạng, chờ khóa, và SendMessage xuyên luồng là các mẫu kinh điển.
  • Đối sách là chuyển việc nặng khỏi luồng UI. Trong C#, async/await + Task.Run; trong Win32, luồng worker + PostMessage. Ngăn tái nhập trong lúc thực thi bằng thiết kế, như tắt nút.
  • Trốn bằng DoEvents là đổi lấy lỗi tái nhập. DisableProcessWindowsGhosting chỉ gỡ hiển thị. Không cái nào là sửa gốc.
  • Với điều tra, dump của “khoảnh khắc treo” là thứ quan trọng nhất. Nguyên nhân gần như luôn được viết trên stack của luồng UI nguyên như vậy.

Từ góc độ người dùng “Không phản hồi” là “nó hỏng”, nhưng một khi bạn biết cơ chế bạn có thể dịch nó thành câu chính xác “luồng UI không trở về trong 5 giây”. Làm việc ngược từ câu đó, các nguyên nhân ứng viên, cách sửa, và thủ tục điều tra đều rơi ra tự nhiên.

Bài viết liên quan

Lĩnh vực tư vấn liên quan

KomuraSoft LLC đảm nhận điều tra nguyên nhân gốc rễ của ứng dụng nghiệp vụ “treo thỉnh thoảng” hoặc thành “Không phản hồi” (phân tích dump và phân tích vết), tái cấu trúc mã UI cũ đầy việc đồng bộ sang async/await và tách luồng worker, và rà soát thiết kế UI không đóng băng. Ngay cả khi bạn chưa có thủ tục tái hiện, chúng tôi có thể giúp bắt đầu từ thiết kế cách thu thập bằng chứng.

Liên kết tham khảo

  1. Microsoft Learn, IsHungAppWindow function (winuser.h). Về tiêu chí phán đoán rằng ứng dụng được coi không phản hồi khi nó “không đang chờ đầu vào, không đang trong chuỗi khởi động, và chưa gọi PeekMessage trong thời gian chờ nội bộ 5 giây”; về tiêu chí 5 giây này có thể đổi; và về hàm luôn trả về TRUE cho cửa sổ ma.  2

  2. Microsoft Learn, GetMessage function (winuser.h). Về hệ thống coi cửa sổ cấp cao nhất không phản hồi khi nó ngừng phản hồi thông điệp trong vài giây và thay nó bằng cửa sổ ma cùng thứ tự Z, vị trí, kích thước và hình dạng; về người dùng chỉ có thể di chuyển, đổi kích thước, hoặc đóng; và về cửa sổ ma không được tạo khi debugger đang gắn.  2 3

  3. Microsoft Learn, About Messages and Message Queues. Về ứng dụng Windows được dẫn bởi sự kiện và thủ tục cửa sổ xử lý thông điệp; về sự phân biệt giữa thông điệp xếp hàng và thông điệp gửi trực tiếp; về việc đổi cửa sổ không phản hồi lấy cửa sổ ma; và về mục trình bày deadlock từ các luồng gửi thông điệp cho nhau.  2 3 4

  4. Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). Về điều khiển WinForms không an toàn để chạm từ luồng nào khác ngoài luồng đã tạo chúng; về dùng Invoke/BeginInvoke cho cập nhật từ luồng khác; và về các mẫu bất đồng bộ an toàn dùng async/await hoặc BackgroundWorker.  2

  5. Microsoft Learn, Using Messages and Message Queues. Về triển khai vòng thông điệp điển hình với GetMessage, TranslateMessage và DispatchMessage, và về cách kiểm tra hàng đợi thông điệp. 

  6. Microsoft Learn, SendMessage function (winuser.h). Về SendMessage gọi thủ tục cửa sổ của cửa sổ chỉ định và không trở về cho tới khi xử lý hoàn tất; về lần gửi tới cửa sổ trên luồng khác làm bên gửi chờ cho tới khi luồng đó xử lý thông điệp; và về sự khác với PostMessage, thứ đặt thông điệp lên hàng đợi mà không chờ trả lời.  2 3

  7. Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). Về việc có thể tắt, cho tiến trình GUI gọi, tính năng cửa sổ ma làm cửa sổ không phản hồi có thể thu nhỏ, di chuyển và đóng; và về việc tắt kéo dài suốt vòng đời tiến trình. 

  8. Microsoft Learn, SendMessageTimeout function (winuser.h). Về việc có thể gửi thông điệp với thời gian chờ; và về cờ (SMTO_ABORTIFHUNG) trở về mà không chờ khi cửa sổ không phản hồi (đã bị phán treo). 

Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.

Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.

Bài viết liên quan trực tiếp đến các dịch vụ sau.

Câu hỏi thường gặp

Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.

"Không phản hồi" xuất hiện trong điều kiện nào?
Hệ điều hành phán một cửa sổ treo khi ứng dụng có cửa sổ không đang chờ đầu vào, không đang trong chuỗi khởi động, và chưa lấy thông điệp (PeekMessage) trong 5 giây. Cửa sổ cấp cao nhất đã treo bị ẩn và được thay bằng "cửa sổ ma" cùng vị trí, kích thước và hình dạng. Chữ "(Không phản hồi)" trên thanh tiêu đề và vẻ trắng mờ thuộc về cửa sổ ma này, thứ chỉ cho phép bạn di chuyển, thu nhỏ, hoặc đóng. Nói cách khác, "Không phản hồi" không phải thứ bản thân ứng dụng hiển thị — đó là màn hình hệ điều hành đặt lên thay ứng dụng.
Có tùy chọn nào để "Không phản hồi" không xuất hiện trong lúc việc đang tiến hành không?
Gọi DisableProcessWindowsGhosting tắt việc đổi sang cửa sổ ma cho tiến trình đó. Điều đó chỉ làm treo kém hiện hơn với người dùng, tuy nhiên — cửa sổ vẫn không phản ứng với đầu vào, và từ góc độ người dùng đó là đóng băng hoàn toàn không có cách di chuyển hay đóng. Sửa thật sự không phải ức chế hiển thị, mà chuyển việc nặng sang luồng worker để luồng UI không bao giờ bị chặn dù chỉ một phần mười giây, chứ đừng nói năm giây. Cũng lưu ý rằng hệ điều hành không tạo cửa sổ ma khi debugger đang gắn, nên có thể trông như "Không phản hồi" không bao giờ xảy ra trong lúc gỡ lỗi.
Có chấp nhận được việc tránh "Không phản hồi" bằng DoEvents (bơm vòng thông điệp thủ công) không?
Không được khuyến nghị. Quay DoEvents hoặc vòng PeekMessage giữa việc nặng sẽ tránh phán đoán cửa sổ treo, nhưng mọi handler sự kiện sau đó có thể tái nhập — lần nhấn nút thứ hai, đóng cửa sổ, bộ hẹn giờ, và tương tự. Handler khác viết lại dữ liệu vẫn đang được xử lý, hoặc chạm form lẽ ra đã đóng rồi ném, tạo lỗi tái nhập phụ thuộc thời điểm và khó tái hiện — tệ hơn bản thân "Không phản hồi". Cách đúng là chuyển bản thân việc sang luồng worker bằng Task.Run hoặc tương tự, và để luồng UI chỉ chịu trách nhiệm hiển thị tiến độ và nhận hủy.
Làm sao cập nhật UI (điều khiển) từ luồng worker?
Điều khiển WinForms và phần tử WPF chỉ được chạm từ luồng đã tạo chúng (thường là luồng UI). Chạm chúng trực tiếp từ luồng worker gây ngoại lệ hoặc hành vi không xác định. Trong C#, async/await là đường dễ nhất: phần tiếp sau await trở về luồng UI gọi, nên bạn có thể cập nhật điều khiển bình thường sau await. Để chuyển tường minh, dùng Control.Invoke/BeginInvoke trong WinForms và Dispatcher.InvokeAsync trong WPF. Trong Win32 native, mẫu đã ổn định là luồng worker PostMessage một thông điệp hoàn tất tùy chỉnh tới luồng UI, và thủ tục cửa sổ cập nhật UI.
Làm sao điều tra vì sao ứng dụng đang hiện "Không phản hồi"?
Điều quan trọng là nắm trạng thái đúng "khoảnh khắc" treo. Trước hết lấy dump đầy đủ từ tab Details của Task Manager bằng "Create dump file", rồi trong WinDbg nhìn stack của luồng UI (luồng đang chạy vòng thông điệp). Liệu nó kẹt trong I/O đồng bộ, chờ mạng, chờ khóa, hay chờ luồng khác qua SendMessage hiện trên stack nguyên như vậy. Để nhìn tiến trình đang sống, danh sách luồng và khung nhìn stack của Process Explorer hữu ích; để theo dõi theo thời gian, bắt một vết WPR hiệu quả. Xem thêm bài nhập môn WinDbg trên trang này, Process Explorer trong thực tiễn, và WPR/WPA trong thực tiễn.

Hồ sơ tác giả

Trang giới thiệu tác giả bài viết.

Go Komura

Đại diện của KomuraSoft LLC

Chuyên về phát triển phần mềm Windows, tư vấn kỹ thuật và điều tra lỗi, đặc biệt trong các dự án có hệ thống hiện hữu và lỗi khó tái hiện.

Liên kết công khai

Quay lại blog