다음을 통해 공유


Windows 애플리케이션의 중지 문제 방지

영향을 받는 플랫폼

클라이언트 - Windows 7
서버 - Windows Server 2008 R2

설명

중단 - 사용자 관점

반응형 애플리케이션과 같은 사용자 메뉴를 클릭하면 현재 작업을 인쇄하는 경우에도 애플리케이션이 즉시 반응하기를 원합니다. 자주 사용하는 워드 프로세서에 긴 문서를 저장하면 디스크가 회전하는 동안 계속 입력하려고 합니다. 애플리케이션이 입력에 적시에 반응하지 않을 때 사용자는 다소 빠르게 참을성이 없습니다.

프로그래머가 애플리케이션이 사용자 입력에 즉시 응답하지 않는 많은 정당한 이유를 인식할 수 있습니다. 애플리케이션이 일부 데이터를 다시 계산하거나 디스크 I/O가 완료되기를 기다리는 중일 수 있습니다. 그러나, 사용자 연구에서, 우리는 사용자가 응답하지의 단지 몇 초 후 짜증과 좌절을 얻을 것을 알고있다. 5초 후에 중단된 애플리케이션을 종료하려고 시도합니다. 충돌 옆에 있는 애플리케이션 중단은 Win32 애플리케이션으로 작업할 때 사용자 중단의 가장 일반적인 소스입니다.

애플리케이션 중단에 대한 여러 가지 근본 원인이 있으며, 모두 응답하지 않는 UI에서 자체적으로 나타나는 것은 아닙니다. 그러나 응답하지 않는 UI는 가장 일반적인 중단 환경 중 하나이며, 이 시나리오는 현재 검색과 복구 모두에 대한 가장 많은 운영 체제 지원을 받습니다. Windows는 자동으로 디버그 정보를 검색하고 수집하며 필요에 따라 중단된 애플리케이션을 종료하거나 다시 시작합니다. 그렇지 않으면 사용자가 중단된 애플리케이션을 복구하기 위해 컴퓨터를 다시 시작해야 할 수 있습니다.

중단 - 운영 체제 관점

애플리케이션(또는 더 정확하게 스레드)이 데스크톱에 창을 만들면 DWM(데스크톱 창 관리자)과 암시적 계약을 체결하여 창 메시지를 적시에 처리합니다. DWM은 메시지(키보드/마우스 입력 및 다른 창의 메시지뿐만 아니라 그 자체)를 스레드별 메시지 큐에 게시합니다. 스레드는 메시지 큐를 통해 해당 메시지를 검색하고 디스패치합니다. 스레드가 GetMessage()를 호출하여 큐를 처리하지 않으면 메시지가 처리되지 않고 창이 중단됩니다. 다시 그릴 수 없으며 사용자의 입력을 수락할 수 없습니다. 운영 체제는 메시지 큐의 보류 중인 메시지에 타이머를 연결하여 이 상태를 검색합니다. 메시지가 5초 이내에 검색되지 않은 경우 DWM은 창을 중단하도록 선언합니다. IsHungAppWindow() API를 통해 이 특정 창 상태를 쿼리할 수 있습니다.

검색은 첫 번째 단계에 불과합니다. 이 시점에서 사용자는 여전히 애플리케이션을 종료할 수 없습니다. X(닫기) 단추를 클릭하면 다른 메시지와 마찬가지로 메시지 큐에 중단되는 WM_CLOSE 메시지가 발생합니다. 바탕 화면 창 관리자는 중단된 창을 원활하게 숨긴 다음 원래 창의 이전 클라이언트 영역의 비트맵을 표시하는 'ghost' 복사본으로 바꾸고 제목 표시줄에 "응답하지 않음"을 추가하는 방법을 지원합니다. 원래 창의 스레드가 메시지를 검색하지 않는 한 DWM은 두 창을 동시에 관리하지만 사용자가 고스트 복사본과만 상호 작용할 수 있도록 합니다. 이 고스트 창을 사용하면 사용자는 응답하지 않는 애플리케이션을 닫고 내부 상태를 변경할 수 없습니다.

전체 유령 환경은 다음과 같습니다.

'메모장에 응답하지 않음' 대화 상자를 보여 주는 스크린샷

바탕 화면 창 관리자는 마지막 작업을 수행합니다. Windows 오류 보고 통합되어 사용자가 애플리케이션을 닫고 필요에 따라 다시 시작할 뿐만 아니라 중요한 디버깅 데이터를 Microsoft로 다시 보낼 수 있습니다. Winqual 웹 사이트에 등록하여 사용자 고유의 애플리케이션에 대한 이 중단 데이터를 가져올 수 있습니다.

Windows 7은 이 환경에 하나의 새로운 기능을 추가했습니다. 운영 체제는 중단된 애플리케이션을 분석하고 특정 상황에서 사용자에게 차단 작업을 취소하고 애플리케이션의 응답성을 다시 지정하는 옵션을 제공합니다. 현재 구현은 차단 소켓 호출의 취소를 지원합니다. 이후 릴리스에서는 더 많은 작업을 사용자 취소할 수 있습니다.

애플리케이션을 중단 복구 환경과 통합하고 사용 가능한 데이터를 최대한 활용하려면 다음 단계를 수행합니다.

  • 애플리케이션이 다시 시작 및 복구를 위해 등록되어 사용자에게 가능한 한 통증이 없는지 확인합니다. 제대로 등록된 애플리케이션은 저장되지 않은 대부분의 데이터를 그대로 유지하여 자동으로 다시 시작할 수 있습니다. 이는 애플리케이션 중단 및 충돌 모두에 대해 작동합니다.
  • Winqual 웹 사이트에서 중단 및 중단된 애플리케이션에 대한 데이터 디버깅뿐만 아니라 빈도 정보를 가져옵니다. 베타 중에도 이 정보를 사용하여 코드를 개선할 수 있습니다. 간략한 개요는 "Windows 오류 보고 소개"를 참조하세요.
  • DisableProcessWindowsGhosting()을 호출하여 애플리케이션에서 고스팅 기능을 사용하지 않도록 설정할 수 있습니다. 그러나 이렇게 하면 평균 사용자가 중단된 애플리케이션을 닫고 다시 시작하지 못하게 되며 종종 다시 부팅됩니다.

중단 - 개발자 관점

운영 체제는 애플리케이션 중단을 5초 이상 메시지를 처리하지 않은 UI 스레드로 정의합니다. 명백한 버그로 인해 신호가 전달되지 않는 이벤트를 기다리는 스레드와 각각 잠금을 유지하고 다른 스레드를 획득하려는 스레드 두 개와 같은 일부 중단이 발생합니다. 너무 많은 노력 없이 이러한 버그를 수정할 수 있습니다. 그러나 많은 중단이 그렇게 명확하지 않습니다. 예, UI 스레드는 메시지를 검색하지 않지만 다른 '중요' 작업을 수행하는 데도 똑같이 사용 중이며 결국 메시지 처리로 돌아갑니다.

그러나 사용자는 이를 버그로 인식합니다. 디자인은 사용자의 예상과 일치해야 합니다. 애플리케이션의 디자인이 응답하지 않는 애플리케이션으로 이어지는 경우 디자인을 변경해야 합니다. 마지막으로, 이것은 중요합니다. 응답하지 않는 것은 코드 버그처럼 수정할 수 없습니다. 디자인 단계에서 선행 작업이 필요합니다. UI의 응답성을 높일 수 있도록 애플리케이션의 기존 코드 베이스를 개조하려고 하면 비용이 너무 많이 드는 경우가 많습니다. 다음 디자인 지침이 도움이 될 수 있습니다.

  • UI 응답성을 최상위 요구 사항으로 만듭니다. 사용자는 항상 애플리케이션을 제어해야 합니다.
  • 사용자가 완료하는 데 1초 이상 걸리는 작업을 취소하거나 백그라운드에서 작업을 완료할 수 있는지 확인합니다. 필요한 경우 적절한 진행률 UI 제공

'항목 복사' 대화 상자를 보여 주는 스크린샷

  • 장기 실행 또는 차단 작업을 백그라운드 작업으로 큐에 추가합니다(작업이 완료되면 UI 스레드에 알리기 위해 잘 생각된 메시징 메커니즘이 필요합니다.)
  • UI 스레드에 대한 코드를 단순하게 유지합니다. 가능한 한 많은 차단 API 호출 제거
  • 준비되고 완전히 작동하는 경우에만 창과 대화 상자를 표시합니다. 리소스를 너무 많이 사용하여 계산할 수 없는 정보를 대화 상자에 표시해야 하는 경우 먼저 일부 일반 정보를 표시하고 더 많은 데이터를 사용할 수 있게 되면 즉시 업데이트합니다. 좋은 예는 Windows Explorer 폴더 속성 대화 상자입니다. 파일 시스템에서 쉽게 사용할 수 없는 폴더의 총 크기 정보를 표시해야 합니다. 대화 상자가 바로 나타나고 작업자 스레드에서 "크기" 필드가 업데이트됩니다.

'크기', '디스크의 크기' 및 '포함' 텍스트가 동그라미로 표시된 Windows 속성의 '일반' 페이지를 보여 주는 스크린샷

아쉽게도 응답형 애플리케이션을 디자인하고 작성하는 간단한 방법은 없습니다. Windows는 차단 또는 장기 실행 작업을 쉽게 예약할 수 있는 간단한 비동기 프레임워크를 제공하지 않습니다. 다음 섹션에서는 중단을 방지하는 몇 가지 모범 사례를 소개하고 몇 가지 일반적인 문제를 강조 표시합니다.

모범 사례

UI 스레드를 단순하게 유지

UI 스레드의 주요 책임은 메시지를 검색하고 디스패치하는 것입니다. 다른 종류의 작업은 이 스레드가 소유한 창을 매달아 두는 위험을 초래합니다.

해야 하는 질문:

  • 장기 실행 작업을 초래하는 리소스 집약적 또는 바인딩되지 않은 알고리즘을 작업자 스레드로 이동
  • 가능한 한 많은 차단 함수 호출을 식별하고 작업자 스레드로 이동하려고 합니다. 다른 DLL로 호출하는 함수는 의심스러워야 합니다.
  • 작업자 스레드에서 모든 파일 I/O 및 네트워킹 API 호출을 제거하기 위해 추가 작업을 합니다. 이러한 함수는 분이 아닌 경우 몇 초 동안 차단할 수 있습니다. UI 스레드에서 모든 종류의 I/O를 수행해야 하는 경우 비동기 I/O를 사용하는 것이 좋습니다.
  • UI 스레드는 프로세스에서 호스트하는 모든 STA(단일 스레드 아파트) COM 서버도 서비스하고 있습니다. 차단 전화를 걸면 메시지 큐를 다시 서비스할 때까지 이러한 COM 서버가 응답하지 않습니다.

안 함:

  • 매우 짧은 시간 동안 커널 개체(예: Event 또는 Mutex)를 기다립니다. 기다려야 하는 경우 새 메시지가 도착할 때 차단을 해제하는 MsgWaitForMultipleObjects()를 사용하는 것이 좋습니다.
  • AttachThreadInput() 함수를 사용하여 스레드의 창 메시지 큐를 다른 스레드와 공유합니다. 큐에 대한 액세스를 제대로 동기화하는 것은 매우 어려울 뿐만 아니라 Windows 운영 체제가 중단된 창을 제대로 감지하지 못하도록 방지할 수도 있습니다.
  • 작업자 스레드에서 TerminateThread()를 사용합니다. 이러한 방식으로 스레드를 종료하면 잠금 또는 신호 이벤트를 해제할 수 없으며 분리된 동기화 개체가 쉽게 발생할 수 있습니다.
  • UI 스레드에서 '알 수 없는' 코드를 호출합니다. 애플리케이션에 확장성 모델이 있는 경우 특히 그렇습니다. 타사 코드가 응답성 지침을 따른다는 보장은 없습니다.
  • 모든 종류의 차단 브로드캐스트 호출을 만듭니다. SendMessage(HWND_BROADCAST)는 현재 실행 중인 모든 잘못 작성된 애플리케이션의 자비를 구합니다.

비동기 패턴 구현

UI 스레드에서 장기 실행 또는 차단 작업을 제거하려면 이러한 작업을 작업자 스레드로 오프로드할 수 있는 비동기 프레임워크를 구현해야 합니다.

해야 하는 질문:

  • 특히 SendMessage를 차단되지 않는 피어 중 하나인 PostMessage, SendNotifyMessage 또는 SendMessageCallback 중 하나로 바꿔 UI 스레드에서 비동기 창 메시지 API를 사용합니다.
  • 백그라운드 스레드를 사용하여 장기 실행 또는 차단 작업을 실행합니다. 새 스레드 풀 API를 사용하여 작업자 스레드 구현
  • 장기 실행 백그라운드 작업에 대한 취소 지원을 제공합니다. I/O 작업을 차단하려면 I/O 취소를 사용하지만 최후의 수단으로만 사용합니다. '올바른' 작업을 취소하는 것은 쉽지 않습니다.
  • IAsyncResult 패턴을 사용하거나 이벤트를 사용하여 관리 코드에 대한 비동기 디자인 구현

잠금을 현명하게 사용

애플리케이션 또는 DLL은 내부 데이터 구조에 대한 액세스를 동기화하기 위해 잠금이 필요합니다. 여러 잠금을 사용하면 병렬 처리가 증가하고 애플리케이션의 응답성이 향상됩니다. 그러나 여러 잠금을 사용하면 다른 순서로 잠금을 획득하고 스레드가 교착 상태에 빠질 가능성이 높아질 수 있습니다. 두 스레드가 각각 잠금을 보유한 다음 다른 스레드의 잠금을 획득하려고 하면 해당 작업은 이러한 스레드에 대한 앞으로 진행을 차단하는 순환 대기를 형성합니다. 애플리케이션의 모든 스레드가 항상 동일한 순서로 모든 잠금을 획득하도록 하여 이 교착 상태를 방지할 수 있습니다. 그러나 '올바른' 순서로 잠금을 획득하는 것이 항상 쉬운 것은 아닙니다. 소프트웨어 구성 요소를 구성할 수 있지만 잠금 획득은 할 수 없습니다. 코드에서 다른 구성 요소를 호출하는 경우 해당 잠금에 대한 가시성이 없더라도 해당 구성 요소의 잠금이 암시적 잠금 순서의 일부가 됩니다.

잠금 작업에는 중요 섹션, 뮤텍스 및 기타 기존 잠금에 대한 일반적인 함수보다 훨씬 더 많이 포함되기 때문에 상황이 더욱 어려워집니다. 스레드 경계를 초과하는 모든 차단 호출에는 교착 상태가 발생할 수 있는 동기화 속성이 있습니다. 호출 스레드는 'acquire' 의미 체계를 사용하여 작업을 수행하고 대상 스레드가 해당 호출을 '해제'할 때까지 차단을 해제할 수 없습니다. 많은 User32 함수(예: SendMessage)와 많은 차단 COM 호출이 이 범주에 속합니다.

설상가상으로 운영 체제에는 코드가 실행되는 동안 때때로 유지되는 자체 내부 프로세스별 잠금이 있습니다. 이 잠금은 DLL이 프로세스에 로드될 때 획득되므로 '로더 잠금'이라고 합니다. DllMain 함수는 항상 로더 잠금에서 실행됩니다. DllMain에서 잠금을 획득하는 경우(그렇지 않아야 하는 경우) 로더 잠금을 잠금 순서의 일부로 만들어야 합니다. 특정 Win32 API를 호출하면 LoadLibraryEx, GetModuleHandle, 특히 CoCreateInstance와 같은 함수를 대신하여 로더 잠금을 획득할 수도 있습니다.

이 모든 것을 함께 연결하려면 아래 샘플 코드를 참조하세요. 이 함수는 여러 동기화 개체를 획득하고 커서 검사에서 반드시 명확하지 않은 잠금 순서를 암시적으로 정의합니다. 함수 항목에서 코드는 중요 섹션을 획득하고 함수가 종료될 때까지 해제하지 않으므로 잠금 계층 구조의 최상위 노드가 됩니다. 그런 다음 코드는 Win32 함수 LoadIcon()을 호출합니다. 이 함수는 이 이진 파일을 로드하기 위해 운영 체제 로더를 호출할 수 있습니다. 이 작업은 로더 잠금을 획득합니다. 이 잠금은 이제 이 잠금 계층의 일부가 됩니다(DllMain 함수가 g_cs 잠금을 획득하지 않았는지 확인). 다음으로 코드는 UI 스레드가 응답하지 않는 한 반환되지 않는 차단 스레드 간 작업인 SendMessage()를 호출합니다. 다시 말하지만 UI 스레드가 g_cs 획득하지 않도록 합니다.

bool foo::bar (char* buffer)  
{  
      EnterCriticalSection(&g_cs);  
      // Get 'new data' icon  
      this.m_Icon = LoadIcon(hInst, MAKEINTRESOURCE(5));  
      // Let UI thread know to update icon SendMessage(hWnd,WM_COMMAND,IDM_ICON,NULL);  
      this.m_Params = GetParams(buffer);  
      LeaveCriticalSection(&g_cs);
      return true;  
}  

이 코드를 살펴보면 클래스 멤버 변수에 대한 액세스만 동기화하려는 경우에도 잠금 계층에서 최상위 잠금을 암시적으로 g_cs 것이 분명해 보입니다.

해야 하는 질문:

  • 잠금 계층 구조를 디자인하고 준수합니다. 필요한 모든 잠금을 추가합니다. 단지 Mutex 및 CriticalSections보다 더 많은 동기화 기본 형식이 있습니다. 모두 포함해야 합니다. DllMain()에서 잠금을 수행하는 경우 계층 구조에 로더 잠금 포함
  • 종속성을 사용하여 프로토콜 잠금에 동의합니다. 애플리케이션이 호출하거나 애플리케이션을 호출할 수 있는 모든 코드는 동일한 잠금 계층 구조를 공유해야 합니다.
  • 함수가 아닌 데이터 구조를 잠급니다. 잠금 획득을 함수 진입점에서 멀리 이동하고 잠금을 사용하여 데이터 액세스만 보호합니다. 잠금에서 작동하는 코드가 적으면 교착 상태가 발생할 가능성이 적습니다.
  • 오류 처리 코드에서 잠금 획득 및 릴리스를 분석합니다. 오류 조건에서 복구하려고 할 때 잊어버린 경우 잠금 계층 구조가 자주 발생합니다.
  • 중첩된 잠금을 참조 카운터로 바꾸면 교착 상태가 될 수 없습니다. 목록 및 테이블의 개별적으로 잠긴 요소는 좋은 후보입니다.
  • DLL에서 스레드 핸들을 대기할 때는 주의해야 합니다. 항상 로더 잠금에서 코드를 호출할 수 있다고 가정합니다. 리소스를 참조-계산하고 작업자 스레드가 자체 정리를 수행하도록 하는 것이 좋습니다(그런 다음 FreeLibraryAndExitThread를 사용하여 새로 종료).
  • 자체 교착 상태를 진단하려면 Wait Chain Traversal API 사용

안 함:

  • DllMain() 함수에서 매우 간단한 초기화 작업 이외의 작업을 수행합니다. 자세한 내용은 DllMain 콜백 함수를 참조하세요. 특히 LoadLibraryEx 또는 CoCreateInstance를 호출하지 마세요.
  • 사용자 고유의 잠금 기본 형식을 작성합니다. 사용자 지정 동기화 코드는 코드 베이스에 미묘한 버그를 쉽게 도입할 수 있습니다. 대신 다양한 운영 체제 동기화 개체 선택 사용
  • 전역 변수에 대한 생성자 및 소멸자에서 모든 작업을 수행합니다. 로더 잠금에서 실행됩니다.

예외에 주의

예외를 사용하면 정상적인 프로그램 흐름과 오류 처리를 분리할 수 있습니다. 이러한 분리로 인해 예외 이전에 프로그램의 정확한 상태를 알기 어려울 수 있으며 예외 처리기는 유효한 상태를 복원하는 중요한 단계를 놓칠 수 있습니다. 이는 향후 교착 상태를 방지하기 위해 처리기에서 해제해야 하는 잠금 획득의 경우 특히 그렇습니다.

아래 샘플 코드는 이 문제를 보여 줍니다. "버퍼" 변수에 대한 바인딩되지 않은 액세스로 인해 때때로 AV(액세스 위반)가 발생합니다. 이 AV는 네이티브 예외 처리기에 의해 catch되지만 예외 시 중요한 섹션이 이미 획득되었는지 확인하는 쉬운 방법은 없습니다(AV는 EnterCriticalSection 코드의 어딘가에서 발생할 수도 있음).

 BOOL bar (char* buffer)  
{  
   BOOL rc = FALSE;  
   __try {  
      EnterCriticalSection(&cs);  
      while (*buffer++ != '&') ;  
      rc = GetParams(buffer);  
      LeaveCriticalSection(&cs);  
   } __except (EXCEPTION_EXECUTE_HANDLER)  
   {  
      return FALSE;  
   } 
   return rc;  
}  

해야 하는 질문:

  • 가능하면 __try/__except 제거합니다. SetUnhandledExceptionFilter를 사용하지 마세요.
  • C++ 예외를 사용하는 경우 사용자 지정 auto_ptr 같은 템플릿에서 잠금을 래핑합니다. 잠금은 소멸자에서 해제되어야 합니다. 네이티브 예외의 경우 __finally 문의 잠금을 해제합니다.
  • 네이티브 예외 처리기에서 실행되는 코드는 주의해야 합니다. 예외가 많은 잠금이 유출되었을 수 있으므로 처리기는 잠금을 획득하지 않아야 합니다.

안 함:

  • Win32 API에서 필요하지 않거나 필요한 경우 네이티브 예외를 처리합니다. 치명적인 오류 후 보고 또는 데이터 복구에 네이티브 예외 처리기를 사용하는 경우 대신 Windows 오류 보고 기본 운영 체제 메커니즘을 사용하는 것이 좋습니다.
  • 모든 종류의 UI(user32) 코드에서 C++ 예외를 사용합니다. 콜백에 throw된 예외는 운영 체제에서 제공하는 C 코드 계층을 통해 이동합니다. 해당 코드는 C++ 언롤 의미 체계에 대해 알지 못합니다.