2007. 2. 16. 13:02
DevX Win32 Assembly Tutorial 15: Multithreading Programming

Tutorial 15: Multithreading Programming

이 번장에서는, multi-thread 프로그램에 대해서 설명한다. 주로, thread간의 데이터의 교환방법을 설명한다.
 소스    리소스 스크립트    실행 결과  

Theory:

이전 장에서 프로세스에는 최소한 한개의 메인 thread가 있다는 것을 배웠다. 그 thread는, 일련의 실행코드로서 , 프로그램에서 자유롭게 thread를 만들 수가 있다. 멀티태스킹시스템은 원하는 만큼 얼마든지 프로세스를 동시에 실행시키는 것이 가능하지만, multi-thread는 멀티태스킹과 유사한 방식으로, 한개의 프로세스로 몇개의 thread가 동시에 실행하도록 하는 것이다. 같은 함수를 몇번이고 실행시킬 수 있고, 응답하는 함수를 얼마든지 재실행시킬 수 있다. multi-thread는 Win32 고유의 기능으로서, Win16에는 구현되지 않았다.

thread는 같은 프로세스에서 실행되므로, 글로벌하게 할당할 수 있었던 메모리영역에 확보한 리소스는 어느 thread로부터도 액세스 할 수 있다. 그렇지만, 생성된 thread에는, 각각 고유의 스택영역을 가지고 있어서, thread 마다의 로컬 변수를 가질 수 있으며,당연히 프라이빗(다른 thread로부터 액세스 할 수 없다)형태이다. thread는 고유의 레지스터 세트도 저장/유지하고 있기때문에, thread의 변경이 발생하면 현재까지 작동하고 있던 thread의 레지스터 상태를 저장해 두고 중지상태로 들어가며,다른 처리를 수행하고, 중단했던 thread가 작동될때에, 저장/유지하고 있던 레지스터 상태를 바탕으로 해서 복구하고 처리를 계속한다. 이것은 내부적으로 처리되며 Windows가 대신 해서 처리해준다.(DOS시절의 인터럽트방식과 유사하다)

다음은 thread의 2가지 분류에 대한 설명이다.

  1. 유저 인터페이스 thread
    자신만의 윈도우를 가지며, Windows로부터의 메세지를 받아들인다. 이 타입의 thread는 Win16 뮤텍스룰을 전제로 하고 있기때문에, 단지 하나의 유저 인터페이스 thread 만이 16 bit의 user커널, gdi커널을 사용할 있다.
    Windows95 API 함수군은, 16 bit 코드로 쓰여져 있기때문에 Win16 뮤텍스는 Windows95에서는 독특한 것이다. WindowsNT는 32 bit 코드로 작성되어 있기 때문에, 당연히 Win16 뮤텍스를 사용하지 않기 때문에, NT로 작동하는 유저 인터페이스 thread는 Windows95보다 부드럽게 작동한다.
  2. 워커 thread
    윈도우를 생성하지 않는다. 당연히 윈도우 메세지를 받아들이지도 않는다.대부분 백그라운드에서 주어진 처리를 실행한다.

Win32에서 multi-thread 프로그램을 실행할 때 아래와 같은 방식으로 처리하는 방법을 제안한다. 메인 thread는 유저 인터페이스 처리를 수행하고, 다른 thread는 백그라운드에서 처리하도록 만드는 방법을 취하기 바란다. 이런 방법은, 메인 thread가 간부역활을 하고, 다른 thread는 사원의 역활을 하는 것과 같은 관계로서, 실제 일을 수행하는 것은 사원으로서, 간부의 역활은 사원에게 업무를 할당하는 것으로, 업무의 진척상황을 간부에게 보고하는 방식이다. 만약, 사원에게 일을 할당하는 일 없이 스스로 모두업무를 처리하는 경우는, 여유가 없어져 버린다.

이와 같은 일은 윈도우에서도 발생해서, 만약, 메인 thread에서 시간이 오래 걸리는 처리를 하고 있는 경우, 그 처리가 끝날 때까지 유저의 입력처리할 수 없게 되어 버린다. 그래서, 시간이 오래 걸리는 처리를 수행하는 thread를 새롭게 생성한 후, 메인 thread가 유저 입력을 처리하도록 할 필요가 있다.

어찌 됐든, CreateThread 함수를 호출하면 thread를 생성할 수 있다. 프로토타입은 다음과 같다.

CreateThread proto lpThreadAttributes : DWORD,\
                   dwStackSize        : DWORD,\
                   lpStartAddress     : DWORD,\
                   lpParameter        : DWORD,\
                   dwCreationFlags    : DWORD,\
                   lpThreadId         : DWORD

CreateThread 함수는 CreateProcess 함수와 유사하다.

  • lpThreadAttributes
    thread의 보안속성을 지정한다. 기본 보안기술자를 사용하고 싶다면, NULL로 지정한다.
  • dwStackSize
    thread의 스택크기를 지정한다. 메인 thread와 같은 크기로 한다면, NULL로 지정한다.
  • lpStartAddress
    thread 함수의 포인터로서 , 이 함수가 thread의 주된 업무를 맡게 된다. 32비트 인수 한개로 구성되며, 32비트 변수를 반환하게 되어 있다.
  • lpParameter
    thread 함수에 넘겨주는 인수
  • dwCreationFlags
    thread의 제어를 처리한다. 0 을 지정하면 생성 후 곧바로 실행되고, CREATE_SUSPENDED를 지정하면, 정지한 상태로 된다.
  • lpThreadId
    새롭게 생성한 thread의 thread ID를 저장한다.

CreateThread 함수가 성공적으로 처리되면, 생성한 thread의 핸들을 반환한다. 실패한다면 NULL값이 저장된다. dwCreationFlags에 CREATE_SUSPENDED를 설정하지 않으면, 생성과 동시에 thread 함수가 실행된다. 만약, CREATE_SUSPENDED가 설정되면, ResumeThread 함수가 호출될 때까지 정지 상태로 된다.

thread 함수가 ret 명령어로 제어를 돌려줄 때, thread 함수가 호출 하는 대신에, Windows가 묵시적으로 ExitThread 함수를 호출 한다. 물론, 자신이 생성한 thread 함수에서 ExitThread 함수를 호출 할 수도 있겠지만,무의미 하다.

GetExitCodeThread 함수를 호출하면, thread의 종료 코드를 얻을 수 있다. 다른 thread에 있는 thread를 종료할 수 있더라도, TerminateThread 함수를 호출 하는것은, 칼날의 양면과 같기 때문에 반드시 필요한 경우에만 사용하기 바란다.왜냐하면, TerminateThread 함수를 사용했을 경우, 해당 thread는 클린업 코드를 실행 수행하지 않고 ,곧바로 종료시켜버리기 때문이다.

이번에는 thread간의 공동작업에 대해서 설명한다.
이것은 3가지의 경우에서 사용된다.

  • 글로벌 변수의 사용
  • Windows 메세지
  • 이벤트

먼저, 글로벌 변수를 사용하는 방법에 대한 설명이지만, thread간에 글로벌변수라는 프로세스에서 확보된 리소스를 공유할 수 있으므로, 글로벌 변수를 사용함으로써, 공동 작업을 실행 할 수있다. 하지만, 이런 방법은 「동기」라는 것에 주의해야만 한다. 예를 들어, 10개의 멤버를 가지는 구조체를 2개의 thread가 동시에 사용하고 있다고 가정하자. 그리고, 한 thread가 그 10개의 멤버중 5개의 데이터의 변경 하고 있을 때, Windows가 갑자기 다른쪽의 thread로 전환했을 경우, 어떻게 될 것인가? 변경된 thread는, 잘못된 데이터를 사용하게 된다. multi-thread 프로그램에서 실수는 용서되지 않고,디버그나 유지보수작업이 매우 복잡해 진다. 또한 이런류의 버그는 랜덤하게 발생되기 때문에 찾아내는 일 또한 매우 어렵게 된다.

다음으로 Windows 메세지를 사용하는 경우지만, thread의 타입이 유저 인터페이스 thread라면 아무런 문제가 없고, 양방향 통신이 가능해 진다.사용자 정의된 메세지를 사용함으로써 사용이 가능해 진다. 기본값으로 WM_USER를 사용하고, 아래와 같은 방법으로 Windows 메세지를 사용자화 한다.

WM_MYCUSTOMMSG equ WM_USER+100h

Windows는 WM_USER보다 큰 값은 사용하지 않으므로, WM_USER를 넘는 값을 지정해서 독자적인 Windows 메세지를 할당해서 사용 할 수 있다.

하지만 이런 방법은 단지 유저 인터페이스 thread에서만 가능하고, 워커 thread일경우에는 thread간의 양방향 통신을 할 수 없다. 왜냐하면, 워커 thread는 자신만의 윈도우를 가지지 않기 때문에, 메시지 큐도 당연히 없다.이럴 경우에는, 다음과 같은 방법을 사용한다.

    유저인터페이스 thread ------>          글로벌 변수                         ----> 워커 thread
    워커 thread                ------>         사용자정의된 윈도우 메세지    ----> 유저인터페이스 thread

본 예제에서도 이방법을 사용하고 있다.

마지막으로 이벤트를 이용하는 방법이 있지만,이 방법은 이벤트 오브젝트를 플래그의 일종으로서 다뤄서 이벤트 오브젝트가 「Non Signal」상태라면 thread는 일시 정지상태로 되고 CPU 타임 슬라이스는 할당 할 수 없게 된다. 만약 이벤트 오브젝트가 「Signal」상태인 경우는, Windows는 thread를 깨우게되고, thread는 동작되며 작업을 시작할 수 있게 된다.

Example:

example zip 파일을 다운로드해서 압축을 해제하면, thread1.exe 라는 실행 파일이 있으므로, 이 파일을 실행해 보자. 실행 한후,"Savage Calculation" 라는 메뉴 를 선택한다. 이것은 "add eax, eax" 라고 코드를 6억번 반복하는 코드로서 , 실행하면, 메인 윈도우에 대한 윈도우를 움직인다거나 메뉴를 선택하는 등의 모든 조작을 할 수 없는 것을 확인하기 바란다. 계산코드가 종료되면, 메시지 박스가 표시되고, 이 후부터는 윈도우에 대한 조작을 할 수 있다. (점유코드)

이런, 단점을 해결하기 위해서, 계산코드를 워커 thread로 처리해서, 메인 thread가 유저의 입력을 처리 할 수 있도록 하는 것이 좋다. 메인윈도우의 응답시간이 약간 늦어질지도 모르지만, 그래도 반응은 할 수 있으므로, 유저가 아무것도 못하는 상황보다는 나을 것이다.

.386
.model flat, stdcall
option casemap:none
WinMain proto :DWORD, :DWORD, :DWORD, :DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass", 0
AppName db "Win32 ASM MultiThreading Example", 0
MenuName db "FirstMenu", 0
SuccessString db "The calculation is completed! ", 0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance, eax
   invoke GetCommandLine
   mov CommandLine, eax
   invoke WinMain, hInstance, NULL, CommandLine, SW_SHOWDEFAULT
   invoke ExitProcess, eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   mov  wc.cbSize, SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra, NULL
   mov  wc.cbWndExtra, NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground, COLOR_WINDOW+1
   mov  wc.lpszMenuName, OFFSET MenuName
   mov  wc.lpszClassName, OFFSET ClassName
   invoke LoadIcon, NULL, IDI_APPLICATION
   mov  wc.hIcon, eax
   mov  wc.hIconSm, eax
   invoke LoadCursor, NULL, IDC_ARROW
   mov  wc.hCursor, eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx, WS_EX_CLIENTEDGE, ADDR ClassName, ADDR AppName,\
          WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,\
          CW_USEDEFAULT, 300,200, NULL, NULL,\
          hInst, NULL
   mov  hwnd, eax
   invoke ShowWindow, hwnd, SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .WHILE TRUE
           invoke GetMessage, ADDR msg, NULL, 0,0
           .BREAK .IF (! eax)
           invoke TranslateMessage, ADDR msg
           invoke DispatchMessage, ADDR msg
   .ENDW
   mov    eax, msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   .IF uMsg==WM_DESTROY
       invoke PostQuitMessage, NULL
   .ELSEIF uMsg==WM_COMMAND
       mov eax, wParam
       .if lParam==0
           .if ax==IDM_CREATE_THREAD
               mov eax, OFFSET ThreadProc
               invoke CreateThread, NULL, NULL, eax,\
                                       0,\
                                       ADDR ThreadID
               invoke CloseHandle, eax
           .else
               invoke DestroyWindow, hWnd
           .endif
       .endif
   .ELSEIF uMsg==WM_FINISH
       invoke MessageBox, NULL, ADDR SuccessString, ADDR AppName, MB_OK
   .ELSE
       invoke DefWindowProc, hWnd, uMsg, wParam, lParam
       ret
   .ENDIF
   xor   eax, eax
   ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
       mov ecx, 600000000
Loop1:
       add eax, eax
       dec ecx
       jz  Get_out
       jmp Loop1
Get_out:
       invoke PostMessage, hwnd, WM_FINISH, NULL, NULL
       ret
ThreadProc ENDP

end start

Analysis:

메인 프로그램은 일반적인 윈도우형태로 되어있고 메뉴가 존재한다. 유저가"Create Thread"메뉴를 선택하면 프로그램은 다음과 같은 처리를 수행해서 thread를 생성한다.

.if ax==IDM_CREATE_THREAD
    mov eax, OFFSET ThreadProc
    invoke CreateThread, NULL, NULL, eax, NULL, 0, ADDR ThreadID
    invoke CloseHandle, eax

위의 처리는, ThreadProc이라는 이름의 함수를 실행하는 thread를 생성하는 것이다. 이 처리가 성공적으로 수행되면, CreateThread 함수는 곧바로 제어를 돌려주고, ThreadProc 함수가 실행되게 된다. 예제에서는 thread 핸들을 사용하지 않기 때문에, 곧바로 닫고 있다. 이것은 메모리를 절약해 준다. 단지, thread 핸들을 닫는 것이 thread를 종료하는 것은 아님을 주의 하기 바란다. 그냥, thread 핸들을 나중에 다시 사용할 수 없다는 것일뿐이다.

ThreadProc PROC USES ecx Param:DWORD
       mov ecx, 600000000
Loop1:
       add eax, eax
       dec ecx
       jz  Get_out
       jmp Loop1
Get_out:
       invoke PostMessage, hwnd, WM_FINISH, NULL, NULL
       ret
ThreadProc ENDP

위에서 알수있듯이, ThreadProc 함수는 정상적이지 않는 오랜시간이 걸리는 루프를 돌고있다.루프가 종료될때, WM_FINISH 메세지를 메인 윈도우에 전송한다. WM_FINISH 메세지는 다음과 같이 사용자정의어 있다.

WM_FINISH equ WM_USER+100h

WM_USER에 100을 더할 필요는 없지만, 이렇게 하는것이 보다 안전하다.

추가로, WN_FINISH 메세지는 이 윈도우에서만 유효한 것으로, WM_FINISH 메세지가 메인 윈도우로 보내지면, 계산이 종료되었다는 메시지 박스가 출력된다.

"Create Thread" 함수를 호출한 수만큼(성공했을 경우), thread를 생성할 수 있으므로, 여러개의 thread를 생성할 수 있다. 이 예제에서는, thread간의 교환은 일방적인 방식으로, 생성한 thread가 메인 윈도우의 통지에만 사용된다. 만약 메인 윈도우로부터 워커 thread에 대한, 메세지를 전송하고 싶다면, 다음과 같이 하면 된다.

  • 메뉴에"Kill Thread"라는메뉴를 생성한다.
  • 커맨드를 실행해도 되는지를 판별하기 위한 플래그(커맨드 플래그)를 글로벌역역에 생성한다. TRUE면 thread를 스톱시키고, FALSE면 실행 한다.
  • ThreadProc 함수의 루프에서 그 커맨드 플래그를 설정한다.

유저가"Kill Thread"메뉴를 선택하면, 메인 프로그램은 커맨드 플래그를 TRUE로 설정한다. 그런 후, 커맨드 플래그가 TRUE인지 체크하는 ThreadProc 함수의 루프에서 TRUE를 인식하면, thread를 종료해서, 제어를 되돌려주게 된다.


Posted by openserver