APC(Asynchronous procedure call)에 대하여 -- VC++



SleepEx()에 대해 공부하다가 좋은 글이 있어 정리해 둡니다.

APC 는 asynchronous procedure call 의 약자입니다.
그렇다면 synchronous procedure call(이하 SPC 라고 합시다)는 무엇일까요?

SPC는 일반적으로 우리하 호출하는 함수입니다. 즉 함수의 이름과 parameter를 전달하면 stack을 이용하여 parameter를 push하고 instruction pointer를 호출한 함수로 이동하게 되겠지요. 뭐 일반적인 함수의 호출방식이니 이에 대한 문의는 없을 것 같구용.

APC는 참으로 특이한 녀석입니다. SPC에서는 호출되는 함수의 signature를 정의하는 방법이 특별히 제한되어 있지는 않지만 APC를 사용하기 위해서는 항상 다음과 같은 형태를 띄어야 합니다.

VOID CALLBACK APCProc(ULONG_PTR dwParam);

이 signature를 보면 parameter가 pointer 변수하나를 받기 때문에, 이걸 잘 활용하면 어떤 갯수의 어떤 data든 보낼 수 있습니다. 그럼 이 함수를 어떻게 호출할까요? CALLBACK 으로 정의되어 있으니, 함수이름을 써서 우리가 직접호출하지는 않을것을 예측할 수 있습니다.

이러한 APC procedure를 호출하는 방법이 바로 QueueUserAPC() 를 사용하시면 됩니다.

Signature를 보면

DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);

첫번째 parameter는 APC procedure의 function pointer를 써주면 됩니다.
두번째 parameter는 실제로 사용자가 정의한 APC procedure를 어떤 thread가 호출하게 하겠느냐 입니다.
세번째 parameter는 APC procedure의 parameter로 넘겨줄 값을 쓰면됩니다.

그런데 여기서 또 한가지를 더 알아야 합니다.

우리가 QueueUserAPC()에 적절히 값을 넣어서 호출한다고 해서 APC procedure가 지정한 thread에 의해서 지정한 parameter를 가지고 바로 호출되는 것이 아닙니다. 그럼 언제 호출되는고 하니 사용자가 지정한 thread 가 alertable 상태가 될 때 호출됩니다. 그렇다면 alertable 상태란건 무엇일까요?

windows의 thread 들은 그 thread가 생성되어 파괴될때 까지 자신의 상태를 가지고 있습니다.
"Waiting" 상태는 thread가 특정 event를 받기 이전까지는 아무것도 하지 않는 상태입니다.

이 상태는 사용자가 WaitFor.. 류의 wait function 또는 Sleep류의 함수를 사용하면 thread는 이 상태로 진입하게 됩니다. 이 경우 CPU scheduling 대상에서 이 thread는 제외됩니다.

"Ready" 상태는 CPU scheduing 대상이긴 하나 아직 CPU 의 quantium time을 받지 못한 상태를 말합니다.
이 경우 CPU time만 받으면 바로 실행 상태로 변경되겠지요.

"Running" 상태는 CPU가 현재 이 thread를 수행하고 있는 상태를 말합니다.
Thread가 수행되고 있다는 것은 "Ready" 상태와 "Running" 상태를 오가면서 code가 수행되는 것을 말하겠지요.

그런데,, 쿠쿵, 제4의 상태가 있습니다. 그게 바로 alertable 상태입니다.
위에서 설명한 3가지의 상태는 Windows가 직접적으로 관할을 하지요.

예를들어 Waiting 상태에서 event를 기다리는 thread는 특정 event가 발생했을때 windows가 직접적으로 Ready 상태로 변경을 해주고, 또한 ready 상태에서 Scheduler에 의해서 CPU Time을 받게 되면 Running 상태로 진입하게 됩니다.

하지만 alertable 상태는 사용자가 조정할 수 있는 유일한 thread의 상태입니다.
alertable state로 thread의 상태를 변경하는 방법은 WaitFor...Ex 류의 함수를 사용하면 됩니다.
또는 SleepEx 와 같은 함수도 가능합니다.

가장 간단한 SleepEx를 봅시다.

DWORD SleepEx(
DWORD dwMilliseconds,
BOOL bAlertable
)

이 함수를 호출하게 되면, Thread는 Waiting 상태로 진입하여, 지정한 시간이 지날때 까지 깨어나지 않고 기다립니다. 그런데 만일 두번째 parameter로 TRUE를 전달하게 되면, Thread는 그 유명한 alertable 상태로 진입을 하게됩니다.

alertable 상태로 진입한 thread는 APCQueue라고 불리우는 Queue에 혹시 Queueing 된 정보가 있는지 확인을 하게 됩니다. Queueing된 정보란 QueueUserAPC() 함수에 의해서 전달된 3가지 정보가 하나의 node가 되겠지요, Thread는 Queue로부터 하나의 node를 빼와서 지정한 함수를 호출하는 것입니다.

만일 APC Queue에 Queueing된 node가 10개정도 되고, SleepEx에서 1초정도를 대기시간으로 했다고 가정합시다.
물론 bAlertable을 TRUE로 주었다고 가정하지요. 이 경우 Thread는 1초동안 Alertable 상태이기 때문에, APC Queue로 부터 node를 가져다가 지정된 함수를 호출하여 수행을 계속해갈 것입니다.

그런데 3번째 node를 수행하는 중에, 1초가 되어버렸습니다.
이경우 3번째 node까지만을 수행하고, Thread는 event를 받아 ready 상태로 진입하게 됩니다.

이해가 되셨는지요?

그럼 도움이 되셨길...

"공부합시당..." <@discussions.microsoft.com> wrote in message news:A76CA6D8-7072-444C-8F66-47C4D3DFE171@microsoft.com...

> APC 가 뭐죠?...
>
> QueueUserAPC 가 이것과 관련이 있다고 보여집니다만...
>
> 각각의 스레드는 APC 큐라는 게 있다고 하는 군요.
>
> 약간의 개념이라도 설명을 부탁드립니다.

출처 : http://himskim.egloos.com/1053865

참고자료 : HOWTO: 비동기 프로시저 호출을 이용하여 Waitable 타이머 사용 하기

Waitable 타이머는 임의의 시간 또는 정기적으로 신호를 알리는 커널 개체입니다. 비동기 프로시저 호출 (APC) 는 타이머가 신호를 보낼 때 마다 콜백 함수가 실행되도록 Waitable 타이머와 연동될 수 있습니다. 본 문서의 샘플 코드는 이것을 어떻게 구현하는지 보여 주고 있습니다.

Waitable 타이머들을 사용할 때 상수 WIN32_WINNT 를 0x0400으로 정의할 필요가 있습니다. 이 상수는 적절한 Waitable 타이머 함수 원형이 선언되는지 확인하기 위한 <windows.h> 의 포함 이전에 정의되어야 합니다.

CreateWaitableTimer() 을 호출함으로써 Waitable 타이머을 생성할 수 있습니다. 이 함수는 커널 개체에 핸들을 반환합니다. 타이머가 이미 존재하고 있다면 OpenWaitableTimer() 을 사용함으로써 프로세스 관련 핸들을 구할 수 있습니다. CreateWaitableTimer() 또는 OpenWaitableTimer() 중 어떤 것을 통해서 구해지든 간에 그 핸들은 타이머가 더 이상 필요하지 않게 되었을 때 릴리즈 되어야 합니다. CloseHandle() 을 호출하여 이것을 실행합니다.

타이머는 SetWaitableTimer() 호출로써 설정됩니다. 타이머는 특정한 시간으로 설정될 수 있습니다 (예: December 16, 1999 at 9:45 PM) 또는 상대 시간(예: 지금부터 5 분). SetWaitableTimer() 는 Due Time 을 위한 LARGE_INTEGER 를 필요로 합니다. 이 값은 FILETIME 구조에 의해서 설명된 포맷 내에 존재하고 있어야 합니다. 만일 이 값이 양의 값을 갖는다면 그것은 특정한 시간을 의미하며 음의 값을 갖는다면 100-nanosecond 단위 내에서의 Relative Time을 의미하는 것입니다. 나중에 보시게 될 샘플 코드는 Relative Time 을 사용하고 있습니다. 타이머는 SetWaitableTimer() 호출 이후 5초 동안 신호를 보내게 될 것 입니다.

타이머를 정기적으로 자체적인 신호를 보낼 수 있도록 설정할 수 있습니다. 세 번째 매개 변수로써 정기적인 값(milliseconds 단위)을 SetWaitableTimer()에 넘겨줌으로써 진행합니다. 정기적인 타이머가 필요하다면 CreateWaitableTimer()에 두 번째 매개 변수로써 FALSE 를 넘겨 줌으로써 타이머를 자동으로 설정되는 타이머로 만들어야 합니다. 이 샘플은 2 초 간격으로 타이머를 설정합니다.

타이머가 설정되었을 때 Waitable 타이머와 비동기 프로시저 호출 (APC) 함수를 연동할 수 있습니다. APC 함수는 Completion 루틴(Routine)이라고 합니다. Completion 루틴(Routine)의 주소는 SetWaitableTimer()의 네번째 매개 변수입니다. 다섯 번째 매개변수는 인자(Arguments)를 Completion 루틴(Routine)으로 넘겨 주기 위해서 사용하는 Void Pointer 입니다.

모든 APC들에 있기 때문에 스레드는 Completion Routine 를 실행하기 위해서 Alertable State 에 있어야만 합니다. Completion 루틴(Routine)은 SetWaitableTimer() 라고 하는 동일한 스레드에 의해서 실행될 것입니다. 그래서 이 스레드는 결국에는 그 자체가 Alertable State 로 되어야만 합니다. 다음의 Alertable 함수들 중에 하나를 호출함으로써 이것을 진행할 수 있습니다:

SleepEx()
WaitForSingleObjectEx()
WaitForMultipleObjectsEx()
MsgWaitForMultipleObjectsEx()
SignalObjectAndWait()

각각의 스레드는 APC 큐를 가지고 있습니다. 함수들 중에 하나가 호출되었을 때 스레드의 APC 큐내에 엔트리가 있다면 그 스레드는 Sleep으로 들어가지 않습니다. 대신에 해당 엔트리는 APC 큐에서 제거되며 Completion 루틴(Routine)이 호출됩니다.

어떠한 엔트리도 APC 큐 내에 존재하지 않는 경우 스레드는 Wait 가 충족될 때까지 정지됩니다. Wait 는 APC 큐에 엔트리를 추가함, Timeout, 핸들 신호, 또는 MsgWaitForMultipleObjectsEx()의 경우에는 스레드의 메시지 큐에 메시지를 추가함으로써 충족될 수 있습니다. APC 큐에 있는 엔트리에 의해서 Wait 가 충족되면 스레드는 활성화되고 Completion 루틴(Routine)은 호출됩니다. 이러한 경우에 해당 함수의 반환되는 값은 WAIT_IO_COMPLETION 입니다.

중요: Completion 루틴(Routine)이 실행되고 난 후 시스템은 처리하기 위해서 APC 큐 내에 있는 또 다른 엔트리를 점검합니다. Alertable 함수는 모든 APC 엔트리들이 처리되고 나서야 반환될 것입니다 그러므로 엔트리들이 처리될 수 있는 것 보다 빠른 APC 큐에 엔트리들이 추가되고 있다면 이들 함수들 중 하나의 함수 호출은 절대로 반환되지 않는 것이 가능합니다. 이것은 특별하게 Completion 루틴(Routine)을 실행하는데 필요한 시간보다 그 주기가 짧은 Waitable 타이머들에서 가능합니다.

APC 와 함께 Waitable 타이머를 사용하는 중일 때 스레드는 타이머를 타이머의 핸들상에서 대기하지 않도록 설정합니다. 이것을 사용함으로써, APC 큐에 엔트리가 추가되는 것에 대한 결과가 아닌 타이머가 신호를 내보내는 것에 대한 결과로 인하여 스레드를 활성화하게 됩니다. 결과적으로 스레드는 더 이상 알림 상태(Alertable State)에 있지 않게 되며 Completion 루틴(Routine)은 호출되지 않습니다. 다음의 샘플에서 SleepEx() 는 스레드를 Alertable State 에 넣기 위해서 사용됩니다. 타이머가 신호 처리되고 난 후 스레드의 APC 큐에 엔트리를 추가하였을 때 SleepEx() 은 해당 스레드를 활성화 시킵니다.

샘플 코드

  1. #define _WIN32_WINNT 0x0400
  2. #include <windows.h>
  3. #include <stdio.h>
  4.  
  5. #define _SECOND 10000000
  6.  
  7. typedef struct _MYDATA
  8. {
  9.     TCHAR *szText;
  10.     DWORD dwValue;
  11. } MYDATA;
  12.  
  13. VOID CALLBACK TimerAPCProc(LPVOID lpArg,               // Data value.
  14.                            DWORD dwTimerLowValue,      // Timer low value.
  15.                            DWORD dwTimerHighValue )    // Timer high value.
  16. {
  17.     MYDATA *pMyData = (MYDATA *)lpArg;
  18.     printf( "Message: %s\nValue: %d\n\n", pMyData->szText, pMyData->dwValue );
  19.     MessageBeep(0);
  20. }
  21.  
  22. void main( void )
  23. {
  24.     HANDLE          hTimer;
  25.     BOOL            bSuccess;
  26.     __int64         qwDueTime;
  27.     LARGE_INTEGER   liDueTime;
  28.     MYDATA          MyData;
  29.     TCHAR           szError[255];
  30.  
  31.     MyData.szText = "This is my data.";
  32.     MyData.dwValue = 100;
  33.  
  34.     if ( hTimer = CreateWaitableTimer(NULL,        // Default security attributes.
  35.                                       FALSE,       // Create auto-reset timer.
  36.                                       "MyTimer" )) // Name of waitable timer.
  37.     {
  38.         __try
  39.         {
  40.             // Create a negative 64-bit integer that will be used to
  41.             // signal the timer 5 seconds from now.
  42.             qwDueTime = -5 * _SECOND;
  43.  
  44.             // Copy the relative time into a LARGE_INTEGER.
  45.             liDueTime.LowPart  = (DWORD) ( qwDueTime & 0xFFFFFFFF );
  46.             liDueTime.HighPart = (LONG)  ( qwDueTime >> 32 );
  47.  
  48.             bSuccess = SetWaitableTimer(hTimer,       // Handle to the timer object.
  49.                                         &liDueTime,   // When timer will become signaled.
  50.                                         2000,         // Periodic timer interval of 2 seconds.
  51.                                         TimerAPCProc, // Completion routine.
  52.                                         &MyData,      // Argument to the completion routine.
  53.                                         FALSE );      // Do not restore a suspended system.
  54.  
  55.             if ( bSuccess )
  56.             {
  57.                 for ( ; MyData.dwValue < 1000; MyData.dwValue += 100 )
  58.                 {
  59.                     SleepEx(INFINITE,  // Wait forever.
  60.                             TRUE );    // IMPORTANT!!! The thread must be in an
  61.                                    // alertable state to process the APC.
  62.                 }
  63.             }
  64.             else
  65.             {
  66.                 wsprintf( szError, "SetWaitableTimer() failed with Error %d.", GetLastError());
  67.                 MessageBox( NULL, szError, "Error", MB_ICONEXCLAMATION );
  68.             }
  69.         }
  70.         __finally
  71.         {
  72.             CloseHandle( hTimer );
  73.         }
  74.     }
  75.     else
  76.     {
  77.         wsprintf( szError, "CreateWaitableTimer() failed with Error %d.", GetLastError() );
  78.         MessageBox( NULL, szError, "Error", MB_ICONEXCLAMATION );
  79.     }
  80. }
출처 : http://support.microsoft.com/kb/601487/ko

덧글

댓글 입력 영역