스레드 다루기 (기초편)

@codemaru · May 04, 2012 · 28 min read

윈도우는 스레드를 관리하기 위해서 다양한 함수들을 제공해 준다. 그러한 함수들을 살펴보기에 앞서서 윈도우는 스레드를 위해서 어떠한 속성들을 저장하고 관리하는지를 먼저 살펴보도록 하자. <화면 1>에는 Process Hacker라는 유틸리티를 사용해서 특정 프로세스에서 생성한 스레드 목록을 살펴보고 있다. 이 화면에는 윈도우에서 저장하고 있는 스레드의 속성의 거의 대부분이 포함되어 있다. 각각의 항목이 어떤 의미를 나타내는지 먼저 살펴보도록 하자.

화면 1 스레드 속성들

화면에서 제일 먼저 보이는 것은 TID라는 것이다. 이는 Thread Identifier의 약자로 스레드 아이디를 의미한다. 윈도우에서 동작하는 모든 스레드는 이 아이디를 통해서 다른 스레드와 구분된다. 다음으로는 Start Address가 보인다. 이는 스레드의 시작 주소가 된다. 스레드는 이 주소에서 시작해서 이 함수가 ExitThread를 호출할 때까지 수행을 이어나간다. 다음으로는 Priority가 있다. 이는 우선순위를 나타내는 것으로 해당 스레드가 얼마나 높은 빈도로 스케줄링 될 것인지를 나타낸다. 아래에 있는 Started라고 표시된 항목은 스레드의 시작 시간을 나타낸다. State는 현재 스레드의 상태를 나타낸다. Kernel Time은 스레드가 수행한 커널 코드의 시간을 나타내며, User Time이란 스레드가 수행한 유저 코드의 시간을 나타낸다. Context Switches는 컨텍스트 전환 횟수를 Cycles는 이 스레드가 사용한 CPU 클럭을 나타낸다.

그러면 이제부터 스레드를 어떻게 생성하고 종료시키는지부터 개별 스레드의 이러한 속성들을 어떻게 프로그램 내에서 조작할 수 있는지에 대해서 알아보도록 하자.

스레드의 탄생

CreateThread와 CreateRemoteThread는 윈도우에서 스레드의 탄생을 책임지고 있는 함수다. 이 두 함수는 동일하게 스레드를 생성하는 역할을 한다. 단지 차이가 있다면 CreateThread는 호출한 프로세스에서 동작하는 스레드를 생성하는 역할을 하며, CreateRemoteThread는 파라미터로 넘어온 프로세스에서 수행되는 스레드를 생성하는 역할을 한다. 실제로 윈도우 내부에서 CreateThread는 GetCurrentThread를 사용해서 CreateRemoteThread를 호출하는 것으로 구현되기 때문에 CreateThread는 CreateRemoteThread의 서브셋이라고 보면 되겠다.

<리스트 1>에는 이 두 함수의 원형이 나와 있다. CreateThread의 함수 원형을 살펴보면 6개의 파라미터를 받는 복잡한 함수라는 것을 알 수 있다. 하지만 주로 사용하는 파라미터는 몇 개 되지 않기 때문에 차근차근 사용 방법을 살펴보면 크게 어렵지는 않다. 첫 번째 파라미터는 스레드의 보안 속성이다. 보통은 NULL을 전달하면 된다. 두 번째 파라미터로는 스레드 스택 크기를 지정한다. 0을 넘겨주면 기본 스택 크기로 스레드가 생성된다. 세 번째 파라미터인 lpStartAddress로는 실제 스레드가 수행될 함수 포인터를, 네 번째 파라미터인 lpParameter로는 lpStartAddress로 전달될 파라미터를 넘겨주면 된다. dwCreationFlags는 생성 플래그를 지정한다. 0을 지정하면 기본 상태로 생성한다. 스레드를 정지된 상태로 생성하고 싶으면 이 플래그에 CREATE_SUSPENDED를 지정하면 된다. 끝으로 lpThreadId에는 스레드 아이디를 전달 받을 포인터를 넘겨주면 된다. 스레드 아이디가 필요 없다면 NULL을 지정하면 된다.

리스트 1 CreateThread, CreateRemoteThread 함수 원형

typedef DWORD (__stdcall *LPTHREAD_START_ROUTINE) (LPVOID lpThreadParameter);

HANDLE WINAPI CreateThread(
    LPSECURITY_ATTRIBUTES lpThreadAttributes
    , SIZE_T dwStackSize
    , LPTHREAD_START_ROUTINE lpStartAddress
    , LPVOID lpParameter
    , DWORD dwCreationFlags
    , LPDWORD lpThreadId
);

HANDLE WINAPI CreateRemoteThread(
    HANDLE hProcess
    , LPSECURITY_ATTRIBUTES lpThreadAttributes
    , SIZE_T dwStackSize
    , LPTHREAD_START_ROUTINE lpStartAddress
    , LPVOID lpParameter
    , DWORD dwCreationFlags
    , LPDWORD lpThreadId
);

윈도우에서 동일한 실행 파일을 가진 프로세스를 여러 개 동시에 실행할 수 있는 것처럼 스레드 또한 동일한 lpStartAddress를 가지는 것을 동시에 여러 개 만들 수 있다. 이렇게 만들어진 스레드 인스턴스를 구분하기 위해서는 스레드 아이디라는 것이 사용된다. 스레드 내에서 현재 자신의 스레드 아이디를 구하기 위해서는 GetCurrentThreadId라는 함수를 사용하면 된다. 파라미터는 없으며, 리턴 값은 현재 실행되고 있는 스레드의 아이디를 반환해 준다. 마찬가지로 GetCurrentThread를 사용하면 현재 실행되는 스레드의 의사 핸들을 반환 받을 수 있다.

<리스트 2>에는 이러한 스레드 함수를 사용해서 실제로 스레드를 생성하는 예제가 나와 있다. 프로그램은 PrintThread 함수를 시작 주소로 하는 스레드를 세 개 생성해서 해당 스레드가 종료될 때까지 기다리는 역할을 한다. 세 개의 스레드가 모두 종료되면 프로그램도 같이 종료된다.

리스트 2 PrintThread 프로그램

#include <windows.h>

DWORD
CALLBACK
PrintThread(PVOID param)
{
    ULONG_PTR id = (ULONG_PTR) param;

    for(int i=0; i<3; ++i)
    {
        printf("[TID = %8d] [PARAM = %d] Running...\n"
                , GetCurrentThreadId()
                , id);

        Sleep(1000);
    }

    return 0;
}

int main()
{
    HANDLE threads[3];

    for(int i=0; i<ARRAYSIZE(threads); ++i)
        threads[i] = CreateThread(NULL, 0, PrintThread, (PVOID) i, 0, NULL);

    WaitForMultipleObjects(ARRAYSIZE(threads), threads, TRUE, INFINITE);
    printf("Complete\n");
    return 0;
}

<화면 2>에는 PrintThread 프로그램의 실행 화면이 나와 있다. <화면 2>를 살펴보면 스레드가 생성 순서를 기준으로 0, 2, 1, 1, 2, 0, 0, 2, 1 순서대로 실행된 것을 볼 수 있다. 직접 입력해서 프로그램을 실행해보면 아마 화면의 순서와는 또 다른 실행 흐름을 볼 수 있을 것이다. 이렇게 스레드 프로그래밍은 운영체제 스케줄링 순서에 따라서 매번 실행 흐름이 바뀐다는 특징이 있다. 이런 비결정적인 특성 때문에 멀티 스레드 관련 버그들은 항상 재연이 쉽지 않고 재연이 된다 하더라도 그것을 관찰하려고 하면 버그가 사라지는 하이젠버그 같은 것들이 자주 만들어진다. 이런 특수한 성질이 있기 때문에 멀티스레드 프로그래밍에 있어서는 항상 머릿속으로 모든 실행 흐름을 염두에 두고 꼼꼼하게 따져보는 습관을 가지는 것이 좋다.

화면 2 PrintThread 실행 화면

스레드 종료

스레드 생성 부분을 살펴보았으니 이번에는 스레드를 종료 시키는 방법에 대해서 살펴보도록 하자. 스레드 종료에는 ExitThread, TerminateThread, FreeLibraryAndExitThread등의 함수가 사용된다. <리스트 3>에는 각 함수의 원형이 나와 있다. ExitThread는 가장 기본적인 함수로 해당 함수를 호출한 스레드를 종료하는 역할을 한다. 파라미터로는 스레드 종료 코드가 넘어간다. TerminateThread는 특정 스레드를 강제로 종료 시키는 역할을 한다. TerminateThread의 파라미터로는 종료시킬 스레드 핸들과 종료 코드가 넘어간다. 끝으로 FreeLibraryAndExitThread 함수는 스레드를 종료함과 동시에 특정 모듈을 FreeLibrary 시키는 역할을 한다. DLL 내에서 생성된 스레드에서 해당 스레드 종료와 동시에 모듈을 언로드 시키고 싶을 때 사용하기에 적합한 함수다.

리스트 3 스레드 종료 관련 함수 원형

VOID WINAPI ExitThread(DWORD dwExitCode);
BOOL WINAPI TerminateThread(HANDLE hThread, DWORD dwExitCode);
VOID WINAPI FreeLibraryAndExitThread(HMODULE hModule, DWORD dwExitCode);
BOOL WINAPI GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode);

윈도우 환경에서 프로그래밍을 해본 독자라면 사실 이렇게 복잡한 함수를 직접 호출하지 않고도 그냥 스레드 함수가 리턴 함으로써 스레드가 종료된다는 사실을 알고 있을 것이다. 앞서 작성해본 <리스트 2>의 프로그램도 그렇게 스레드를 종료했다. 또한 대부분의 책에서 그렇게 종료하는 것이 가장 좋은 방법이라고 강조하고 있다. 그렇다면 어떻게 스레드 함수가 리턴하는 것만으로도 스레드가 종료될 수 있을까? 그 마법의 비밀은 다름아닌 윈도우의 CreateThread 구현에 있다. CreateThread 함수는 사실 lpStartAddress를 시작으로 하는 스레드를 생성하지 않는다. 대신 <리스트 4>에 나타난 것과 같이 BaseThreadStart라고 명명된 래퍼 함수를 시작 주소로 하는 스레드를 생성한다. 해당 래퍼 함수는 <리스트 4>에 나와있는 것과 같이 개발자가 실제로 생성하려고 넘겨 주었던 lpStartAddress를 호출하고 해당 함수가 리턴하면 자동으로 ExitThread를 호출하도록 되어 있다. 이런 이유로 우리가 만든 스레드 시작 함수가 리턴 하는 것만으로도 자연스럽게 스레드가 종료될 수 있는 것이다.

리스트 4 BaseThreadStart 함수 의사 코드

VOID
BaseThreadStart(
    LPTHREAD_START_ROUTINE lpStartAddress,
    LPVOID lpParameter
)
{
    ExitThread((lpStartAddress)(lpParameter));
}

한 가지 더 살펴볼만한 내용은 TerminateThread 함수에 관한 오해다. 많은 책에서 TerminateThread를 사용하면 리소스 반환이 정상적으로 되지 않고 마치 큰 일이 생기는 것처럼 언급하고 있지만 이는 사실 잘못된 내용이다. TerminateThread를 호출한다고 해서 리소스 반환이 되지 않고 ExitThread를 호출한다고 해서 리소스 반환이 되는 것은 아니기 때문이다.

<리스트 5>와 같은 스레드를 살펴보자. 이 스레드는 스레드 함수 내에서 메모리를 할당한 다음 리턴한다. 이 스레드 함수가 정상적으로 종료된다면 결국 궁극에는 ExitThread에 의해서 종료된다. 그렇다면 ExitThread 함수는 할당된 buffer를 자동으로 소거해 줄까? 당연히 하지 않는다. 이 스레드의 경우에는 ExitThread로 종료하던, TerminateThread로 종료하던 스레드가 종료되면 메모리 릭이 발생한다.

리스트 5 메모리 할당 작업을 하는 스레드

DWORD
CALLBACK
Thread1(PVOID)
{
    char *buffer = new char[10];
    return 0;
}

윈도우는 프로그램에서 사용하는 리소스와 관련해서 스레드 별로 할당 내용을 추적하지 않는다. 따라서 ExitThread나 TerminateThread 모두 리소스를 소거해 주는 역할을 가지고 있지는 않다. TerminateThread와 ExitThread 사이의 차이는 단지 누가 언제 스레드를 종료 시키느냐의 차이 밖에는 없다. <리스트 6>를 살펴보면 그러한 차이를 알 수 있다. <리스트 6>의 스레드 루틴은 buffer를 할당한 다음 작업을 하고 buffer를 삭제한 다음 종료한다. 이 경우에 있어서 ExitThread에 의해서 종료된다는 것은 항상 자신이 생성한 buffer 리소스에 대해서 해제 작업이 완료된 다음에 종료된다는 것을 알 수 있다. 하지만 외부에서 TerminateThread로 종료 시키는 경우에는 스레드 실행 시점에 따라서 buffer가 해제된 다음일 수도 있고, buffer가 사용 중인 도중일 수도 있다. 따라서 이 경우에는 실행 시점에 따라서 리소스 릭이 발생할 수도 있고, 아닐 수도 있게 되는 것이다.

리스트 6 리소스 해제가 추가된 스레드 루틴

DWORD
CALLBACK
Thread2(PVOID)
{
    char *buffer = new char[10];

    // buffer 관련 작업

    delete [] buffer;    
    return 0;
}

결국은 ExitThread나 TermianteThread나 운영체제 내부적으로 리소스 해제와 관련된 작업을 해주는 것은 없다. 스레드 내에서 생성된 리소스에 대한 책임은 해당 리소스를 생성한 스레드 루틴에 있다. TerminateThread라고 무턱대고 사용하지 말아야 하는 함수라고 생각하기보다는 이러한 내부적인 차이점을 정확하게 이해하고 상황에 맞는 함수를 선택해서 사용하는 습관을 가지는 것이 좋겠다.

끝으로 스레드의 종료 상태를 알아내는 방법에 대해서 살펴보자. 스레드의 종료 상태를 알아내기 위해서는 GetExitCodeThread 함수가 사용된다(<리스트 3> 참고). hThread로 종료 상태를 알고 싶은 스레드를 전달하면 lpExitCode로 종료 상태가 넘어온다. 종료되지 않고 스레드가 실행중인 상태라면 lpExitCode로 STILL_ACTIVE가 전달되며, 종료된 상태라면 스레드 종료 코드가 넘어온다.

실전에서는 GetExitCodeThread 함수보다는 WaitForSingleObject가 훨씬 더 많이 사용된다. 스레드 객체는 윈도우 운영체제에 의해서 종료 시에 시그널 상태로 설정된다. 따라서 WaitForSingleObject로 스레드 핸들을 전달해서 WAIT_OBJECT_0이 반환된다면 해당 스레드는 종료된 것이며, 다른 값이 반환된다면 여전히 실행 중인 상태로 판단할 수 있다.

스레드 일시 정지와 재개

스레드는 언제든지 SuspendThread 함수를 통해서 일시 정지 시킬 수 있고, 또 ResumeThread를 통해서 실행을 재개할 수 있다. 각 함수의 원형은 <리스트 7>에 나와 있다. 파라미터로 전달되는 hThread는 실행을 중지 하거나 재개시킬 스레드 핸들이며, 반환 값은 이전에 실행이 중지된 횟수를 반환한다. 중지된 횟수를 반환한다는 의미는 SuspendThread를 두 번 호출했다면 ResumeThread를 두 번 호출해야 스레드가 실제로 다시 수행된다는 것을 의미한다.

리스트 7 SuspendThread/ResumeThread 함수 원형

DWORD WINAPI SuspendThread(HANDLE hThread);
DWORD WINAPI ResumeThread(HANDLE hThread);

<리스트 8>에는 SuspendThread와 ResumeThread를 사용해서 스레드를 정지 시키고 다시 재개시키는 프로그램이 나와 있다. 프로그램의 코드를 바꿔가면서 SuspendThread와 ResumeThread의 반환 값이 어떤 식으로 동작하는지 살펴보도록 하자.

리스트 8 SuspendResumeThread 프로그램

int main()
{
    HANDLE thread;
    ULONG count;

    thread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, NULL);

    count = SuspendThread(thread);
    printf("suspend count = %d\n", count);

    count = SuspendThread(thread);
    printf("suspend count = %d\n", count);

    count = ResumeThread(thread);
    printf("suspend count = %d\n", count);

    count = ResumeThread(thread);
    printf("suspend count = %d\n", count);

    WaitForSingleObject(thread, INFINITE);
    return 0;
}

스레드 컨텍스트

하나의 CPU 상에서도 스레드를 동시에 실행시키기 위해서는 스레드를 중지시킨 이후에 다시 실행을 재개 시키기 위해서는 어디서부터 실행을 재개할지 저장해 두어야 한다. 이렇게 다시 실행 시킬 위치를 저장해 두는 것을 컨텍스트라고 표현한다. x86/x64 환경에서는 이러한 컨텍스트로 CPU 레지스터를 저장해둔다. 해당 레지스터만 있으면 언제든지 다시 실행을 재개할 수 있기 때문이다.

윈도우에서는 특정 스레드의 컨텍스트를 구하기 위해서 GetThreadContext를 반대로 특정 스레드의 컨텍스트를 설정하기 위해서는 SetThreadContext라는 함수를 사용한다. <리스트 9>에는 해당 함수의 원형이 나와 있다. GetThreadContext 함수는 hThread로 지정된 스레드의 컨텍스트를 lpContext에 저장하는 역할을 한다. 이 함수를 사용할 때 한 가지 주의해야 할 점은 lpContext의 ContextFlags는 함수 호출 전에 미리 설정해 두어야 한다는 점이다. GetThreadContext가 lpContext의 ContextFlags에서 지정한 것과 관계된 레지스터 값만 반환하기 때문이다. SetThreadContext 함수는 hThread로 지정된 스레드의 컨텍스트를 lpContext로 대체시키는 역할을 한다. 이 두 함수 모두 현재 스레드나 실행 중인 스레드에 대해서 함수 호출을 할 경우에는 정상적인 컨텍스트를 구할 수 없다. 따라서 이 함수를 호출하기 위해서는 항상 대상 스레드를 일단 SuspendThread로 중지 시킨 다음에 작업해야 한다는 점을 명심하자.

리스트 9 스레드 컨텍스트 관련 함수들

BOOL WINAPI GetThreadContext(HANDLE hThread, LPCONTEXT lpContext);
BOOL WINAPI SetThreadContext(HANDLE hThread, const CONTEXT *lpContext);

<리스트 10>에는 GetThraedContext/SetThreadContext 함수를 사용해서 실행중인 스레드의 컨텍스트를 런타임에 변경하는 작업을 보여준다. SetThreadContext로 지정하기 전에 Eip를 Bypass로 바꾸었기 때문에 ResumeThread가 수행되면 스레드는 Bypass 함수부터 스레드 수행을 재개한다. <화면 3>에는 DebugView를 통해서 프로그램 실행 중에 디버그 출력을 캡쳐한 화면이 나와 있다. 화면에 나타난 것을 살펴보면 정상적이라면 실행되지 않았을 Bypass 함수가 실행된 것을 알 수 있다.

리스트 10 Bypass 프로그램

#include <windows.h>

ULONG
Bypass()
{
    OutputDebugStringA("Bypass\n");
    ExitThread(0);
    return 0;
}

ULONG
CALLBACK
MyThread(PVOID)
{
    ULONG ntick = GetTickCount() + 1000;

    for(int i=0; i<10; ++i)
    {
        OutputDebugStringA("Hello\n");
        
        while(GetTickCount() < ntick)
            ;

        ntick = GetTickCount() + 1000;
    }

    return 0;
}

int main()
{
    HANDLE thread;
    
    thread = CreateThread(NULL, 0, MyThread, NULL, 0, NULL);
    Sleep(3000);

    SuspendThread(thread);

    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(thread, &ctx);

    ctx.Eip = (ULONG)(ULONG_PTR) Bypass;
    SetThreadContext(thread, &ctx);

    ResumeThread(thread);
    WaitForSingleObject(thread, INFINITE);
    return 0;
}

화면 3 Bypass 프로그램 실행 화면

<리스트 10>의 프로그램에서는 Bypass 함수에서 ExitThread를 호출해서 스레드를 강제로 종료시킨다. 이렇게 하지 않고 Bypass 함수를 수행한 다음 원래 스레드 컨텍스트로 돌아가서 MyThread의 남은 코드를 마저 실행 시키도록 만들 방법은 없는지 생각해보자.

스레드 실행 시간

스레드 실행 시간을 구하는 데에는 GetThreadTimes 함수가 사용된다. 함수 원형은 아래 나와 있는 것과 같다. hThread로는 실행 시간을 구하고 싶은 스레드의 핸들을 전달해 주면, lpCreationTime에는 해당 스레드가 생성된 시간이, lpExitTime에는 해당 스레드가 종료된 시간이, lpKernelTime에는 해당 스레드가 사용한 커널 영역 코드의 시간이, lpUserTime은 해당 스레드가 사용한 유저 영역 코드의 시간이 넘어온다.

BOOL WINAPI GetThreadTimes(
    HANDLE hThread
    , LPFILETIME lpCreationTime
    , LPFILETIME lpExitTime
    , LPFILETIME lpKernelTime
    , LPFILETIME lpUserTime
);

이 함수를 사용하면 특정 스레드의 CPU 점유율을 계산할 수 있다. CPU 점유율이란 “사용한 CPU 시간 / 전체 CPU 시간”으로 계산할 수 있고, 사용한 CPU 시간은 앞서 살펴본 GetThreadTimes 함수의 lpKernelTime과 lpUserTime을 합한 값으로 계산할 수 있다. <리스트 11>에는 이러한 기본적인 원칙을 바탕으로 작성한 현재 스레드의 CPU 점유율을 출력해주는 CpuUsagePrinter 클래스 코드가 나와 있다. CpuUsagePrinter의 순간 점유율은 Reset 이후부터 Dump를 수행할 때까지의 점유율을 최근 1분은 최근 1분간 스레드의 CPU 점유율을 보여준다.

CpuUsagePrinter에서 출력되는 CPU 점유율은 싱글 코어를 기준으로 한 점유율을 말한다. 멀티코어라면 이 점유율이 코어 개수에 대한 비율만큼 줄어들어야 한다. 예를 들어 CpuUsagePrinter가 100%의 점유율을 출력했는데 듀얼코어라면 실제로 듀얼코어 상의 점유율은 50%가 된다는 의미다.

리스트 11 CpuUsagePrinter

typedef LPCWSTR xcwstr;
typedef UINT64 xuint64;

template <class T>
class CpuUsagePrinter
{
public:
    FILETIME fkt_, fut_, fft_;
    FILETIME skt_, sut_, sft_;
    FILETIME ekt_, eut_, eft_;

    T &printer_;
    xcwstr tag_;

    CpuUsagePrinter(xcwstr tag, T &printer)
    : printer_(printer), tag_(tag)
    {
        GetSystemTimeAsFileTime(&sft_);

        FILETIME ct, et;
        GetThreadTimes(GetCurrentThread(), &ct, &et, &skt_, &sut_);

        memcpy(&fft_, &sft_, sizeof(fft_));
        memcpy(&fkt_, &skt_, sizeof(fkt_));
        memcpy(&fut_, &sut_, sizeof(fut_));
    }

    xuint64 ToUInt64(FILETIME &t)
    {
        ULARGE_INTEGER u;
        u.HighPart = t.dwHighDateTime;
        u.LowPart = t.dwLowDateTime;
        return u.QuadPart;
    }

    xuint64 GetDiff64(FILETIME &s, FILETIME &e)
    {
        return ToUInt64(e) - ToUInt64(s);
    }

    void Reset()
    {
        GetSystemTimeAsFileTime(&fft_);

        FILETIME ct, et;
        GetThreadTimes(GetCurrentThread(), &ct, &et, &fkt_, &fut_);
    }

    void Dump(xcwstr tag = NULL)
    {
        xuint64 lusage;
        xuint64 iusage;


        FILETIME ct, et;
        GetThreadTimes(GetCurrentThread(), &ct, &et, &ekt_, &eut_);

        GetSystemTimeAsFileTime(&eft_);

        xuint64 ltime = GetDiff64(sft_, eft_);
        if(ltime == 0)
            return;

        xuint64 lcpu = GetDiff64(skt_, ekt_) + GetDiff64(sut_, eut_);
        if(lcpu < ltime)
            lusage = lcpu * 10000 / ltime;
        else
            lusage = 10000;

        xuint64 itime = GetDiff64(fft_, eft_);
        if(itime == 0)
            return;

        xuint64 icpu = GetDiff64(fkt_, ekt_) + GetDiff64(fut_, eut_);
        if(icpu < itime)
            iusage = icpu * 10000 / itime;
        else
            iusage = 10000;

        printer_.Write("%ls 순간: %3llu.%02llu%%    최근 1분: %3llu.%02llu%%\n"
                        , tag ? tag : tag_
                        , iusage / 100
                        , iusage % 100
                        , lusage / 100
                        , lusage % 100);

        if(ltime > 60 * 10000000)
        {
            memcpy(&skt_, &ekt_, sizeof(skt_));
            memcpy(&sut_, &eut_, sizeof(sut_));
            memcpy(&sft_, &eft_, sizeof(sft_));
        }
    }

    ~CpuUsagePrinter()
    {
        Dump(tag_);
    }
};

<리스트 12>에는 CpuUsagePrinter를 사용해서 간단한 스레드의 CPU 점유율을 측정하는 예제가 나와 있다. CalcThread를 실행하면 <화면 4>에 나와 있는 것과 같이 스레드 동작 과정에서 CPU 점유율이 지속적으로 표시된다. CalcThread 코드를 바꿔가면서 CPU 점유율이 어떻게 변화하는지 살펴보도록 하자.

리스트 12 CalcThread 프로그램

#include <windows.h>

class printer
{
public:
    void write(lpcstr fmt, ...)
    {
        va_list ap;

        va_start(ap, fmt);
        vprintf(fmt, ap);
        va_end(ap);
    }
};

DWORD
CALLBACK
CalcThread(PVOID param)
{
    CpuUsagePrinter<Printer> cpu(L"CalcThread", Printer());
    ULONG_PTR id = (ULONG_PTR) param;

    for(int i=0; i<100; ++i)
    {
        cpu.Reset();
        int k = rand();
        int n = rand() * 10000;
        for(int i=0; i<n; ++i)
        {
            k = k * k;

            if(i == n / 2)
                Sleep(10);
        }

        cpu.Dump();
        Sleep(100);
    }

    return 0;
}

int main()
{
    HANDLE thread;
    thread = CreateThread(NULL, 0, CalcThread, NULL, 0, NULL);
    WaitForSingleObject(thread, INFINITE);
    return 0;
}

화면 4 CalcThread 실행 화면

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중