반응형




 

출처 : 원도우 사용자 그룹(http://www.aspkorea.org)


본내용은 회원으로 가입하고있는 server관련 웹모임의 운영진에서 집필된 것임으로 인용시 출처를 밝혀주시기 바랍니다.




5.6 세마포어 

세마포어는 다익스트라(Dijkstra)에 의해 제시된 개념으로, 철학자들이 굶어죽는 문제를 처음 제기한 이가 다익스트라이다. 세마포어는 병렬적인 리소스와 그 리소스를 차지하는 스레드 사이의 다대다 관계로 해석하면 된다. 세마포어는 철도의 신호기에서 온 것으로 하나의 철길을 두 대 이상의 열차가 사용하기 위해 신호를 따른다는 의미이다. 

뮤텍스가 한 번에 단 하나의 보호되는 자원을 설정할 수 있음에 반해, 세마포어는 해당 자원을 몇 개의 스레드가 동시에 사용할 수 있을지 정의한다. 일반적으로 뮤텍스를 하나의 스레드만이 접근할 수 있는 까닭에 바이너리 세마포어라고 일컫기도 하지만, 윈도우 시스템에서는 최대 하나의 스레드만이 접근할 수 있는 세마포어와 뮤텍스는 완전히 다른 개체로 인정되므로, 처음부터 뮤텍스로 설정한 것과 세마포어로 설정한 것은 차이점을 갖는다는 사실을 잊어서는 안 된다. 

뮤텍스나 세마포어는 스레드를 순서대로 동작시키는 것에 주안점을 맞추므로, 실제 CPU의 활용 상황에서 그렇게 최적화되지 못하는 경우가 많다. 하나의 리소스를 잠금 설정을 했을 경우, 해당 리소스에 복수 접근이 가능하더라도 최대 접근 수는 항상 1로 설정되기 때문에 결국 멀티스레드 프로그램에서 나타나는 복수 스레드가 특정 자원을 뮤텍스로 감쌌을 경우 여러 스레드 사이에서 대기 상태가 계속 나타나게 된다. 이는, 컨텍스트 전환 이후 아이들링 상태의 CPU 동작을 빈번히 가져오게 되며 결국 프로그램은 멀티스레드의 이점을 퇴대한 활용할 수 없다. 

세마포어는 하나의 자원, 혹은 루틴에 최대 접근 숫자를 정의해 놓고, 접근 숫자가 모두 차면 여분의 접근 숫자를 0으로 설정한다. 이후 여분 접근 숫자가 0이 아니면 그만큼의 스레드의 요청을 받아들일 수 있다. 따라서, 잘못 설계하게 되면 세마포어는 멀티스레드가 갖는 문제를 그대로 나타내게 된다. 단지 동시에 동작할 수 있는 스레드의 수만 한정해 놓는 셈이다. 따라서, 상호 간섭이 나타나는 리소스에는 세마포어를 사용할 수 없다. 가령, 열차 화장실 문제로 다시 복귀해서, 화장실이 모자라서 하나를 증설하는 경우 뮤텍스나 크리티컬 섹션으로는 동작하지 않고 세마포어로 동작하게 된다. 그러나, 화장실로 가는 스레드가 동시에 두 개까지 허용된다는 것은 실제 화장실이 두 개인 것을 의미한다. 따라서, 화장실이 하나임에도 불구하고 스레드를 두 개 만들면 어쩔 수 없이 경쟁 상태를 맞게 된다. 

세마포어가 가장 효율적으로 사용되는 것은 데이터베이스의 커넥션 등의 작업이다. 보통 데이터베이스 접근과 같은 작업들을 멀티스레딩으로 처리하는 경우가 많은데, 실제로 데이터베이스 연결은 라이선스, 혹은 성능에 의해 최대 연결 가능 숫자가 지정되어 있다. 따라서, 정의한 숫자 이내의 연결을 위해서 리소스를 사용하는 스레드를 컨트롤해야 한다. 단 하나의 리소스만을 사용할 경우에는 뮤텍스를 쓸 수 있겠지만, 리소스가 두 개 이상이 될 경우에는 효율적인 시스템 운영을 위해서 세마포어를 사용할 수밖에 없다. 

세마포어는 뮤텍스와 약간 다른 방법으로 동작한다. 뮤텍스가 전역 변수로 설정된 것에 반해 세마포어는 지역 변수로 설정된다. 세마포어는 동시에 다중 참조가 가능한 커널 객체이므로, 세마포어 이름을 알고 있다면 일단 생성되어 있는 세마포어를 열어 사용할 수 있기 때문이다. 따라서, 주 스레드에서 세마포어를 연 뒤 파생되는 스레드에서 세마포어를 사용할 수 있는 체계가 확립되는 셈이다. 

이제 뮤텍스를 사용했던 예제를 세마포어까지 확대시켜 보자. 






#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hSemaphore; DWORD threadId = NULL; BOOL bOK; int j; hSemaphore=CreateSemaphore(NULL, // ACL 1, // Initial Locking Count 1, // Maximum Locking Count "MySemaphore"); // 세마포어명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); //스레드 죽기 전에 세마포어가 삭제되면 안됨 bOK=CloseHandle(hSemaphore); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hSemaphore=OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "MySemaphore"); WaitForSingleObject(hSemaphore, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } long lCount; ReleaseSemaphore(hSemaphore, 1, &lCount); CloseHandle(hSemaphore); return 0; } 

전체적인 구성에 있어서는 그다지 차이를 나타내지 않는다. 세마포어는 참조가 가능한 형태로 운영되는 커널 객체이므로 일단 주 스레드에서 세마포어를 선언하고, 해당 CreateSemaphre() 함수를 사용하여 세마포어를 생성하였다. 세마포어는 참조 숫자를 갖고 있기 때문에 현재 자신이 얼마나 참조되고 있는지 ReleaseSemaphore() 함수로 확인할 수 있다. 다만 주의할 점은 ReleaseSemaphore() 함수를 사용하고 난 뒤에는 세마포어가 보호하는 루틴으로부터 빠져 나온 것이기 때문에 컨텍스트 전환의 문제를 맞이할 수 있다는 것이다. 따라서 이 참조 숫자를 사용할 필요가 있을 때 역시 컨텍스트 전환을 방지할 수 있는 방법론을 도입할 필요가 있다. 

OpenSemaphore() 함수는 세마포어를 어떻게 접근할 것인지 접근 옵션을 지정하고 있으며, 이는 일반적인 커널 객체의 경우 거의 비슷한 방법으로 접근이 가능하다. 세마포어의 경우에는 SEMAPHORE_ALL_ACCESS, SEMAPHORE_MODIFY_STATE, SYNCHRONIZE 모두 세 가지가 지원이 가능하며, 모든 접근 가능, 카운터 변경 가능, 대기 함수에서 핸들 사용 가능의 옵션이다.


 


5.7 이벤트 

이벤트는 지금까지 언급했던 동기화 객체들과는 다소 차이가 있다. 지금까지 말한 커널 객체나 크리티컬 섹션은 운영체제에 의해 영위되는 전자동식 운영이었다면 이벤트는 자동이 아니라 수동으로 운영된다. 즉 프로그래머의 의도에 의한 컨텍스트 전환이 이루어진다. 

이벤트는 일종의 전기 스위치로 볼 수 있다. 이 전기 스위치는 두 가지가 있으며, 일반적으로 보통 볼 수 있는 ON/OFF 스위치와, 작업이 끝나면 자동으로 꺼지는 스위치가 있다. 윈도우에서는 전자를 수동 재설정 이벤트(Manual Reset Event)라고 하며 후자를 자동 이벤트(Automatic Event)라고 한다. 이 두 가지의 차이점은 아주 명백하여, 수동 재설정 이벤트는 명시적으로 켜거나 끄지 않으면 항상 원래 있던 상태를 유지하고 있으며, 자동 이벤트는 항상 꺼져 있는 상태로 유지되다가, 스위치를 누르면 특정 작업을 수행하는 순간만 켜져 있고, 작업이 끝나면 도로 꺼져버리는 특성이 있다. 

이벤트 역시 핸들러로 지원되는 커널 객체이다. 따라서, 여지껏 해온 문맥대로라면 CreateEvent() 함수로 설정하고 OpenEvent() 함수로 이벤트를 열 것이다. CreateEvent() 함수에서 이벤트가 어떤 종류(수동 이벤트, 자동 이벤트)로 설정될 것인지 나타낼 수 있으며, 이벤트를 다루는 것은 다음 세 가지 추가 함수를 사용한다. 



  • SetEvent() 
  • ResetEvent() 
  • PulseEvent()

SetEvent() 함수는 이벤트를 켜는 명령어이다. 수동 이벤트라면 계속 켜져 있을 것이며, 자동 이벤트라면 단일 스레드가 작동한 뒤 다시 꺼질 것이다. ResetEvent() 명령은 이벤트를 끄는 것으로, 자동 이벤트의 경우에는 별다른 영향을 주지 못한다. PulseEvent 명령어는 자동 이벤트의 경우 SetEvent()와 동일한 효과를 나타내며, 수동 이벤트는 대기열에 있는 스레드를 동작시키게 된다.


이벤트는 프로세스 사이, 혹은 스레드 사이에서 통신할 수 있으며, 프로세스 사이에서 통신하는 것도 스레드와 동일한 코드를 사용하게 된다. 이제 질리도록 친근해진 소스를 이벤트를 사용하는 방법으로 변형해 보기로 하자. 






#include <windows.h> #include <iostream> using namespace std; DWORD WINAPI myfunction(LPVOID); int i=100; int main(){ HANDLE hThread[5]; HANDLE hEvent; DWORD threadId = NULL; BOOL bOK; int j; hEvent=CreateEvent(NULL, // ACL FALSE, // 자동 이벤트 TRUE, // 초기 상태: ON "MyEvent"); // 이벤트명 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hEvent); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, "MyEvent"); WaitForSingleObject(hEvent, INFINITE); int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } SetEvent(hEvent); CloseHandle(hEvent); return 0; } 

WaitForSingleObject()는 Event의 경우 ON 상태에서 감지된다. 따라서, 자동 이벤트를 사용하였으므로 일단 이벤트가 켜진 상태로 프로그램을 설정한다. 그러면 제일 처음 스레드가 실행되면서 이벤트가 자동으로 꺼지게 된다. 이후 발생되는 스레드는 모두 대기 상태로 들어갈 수밖에 없다. 처음 스레드가 종료되어야 SetEvent() 함수가 실행되어 다시 스레드를 원래 상태로 돌려놓을 것이기 때문이다. 

이벤트는 이처럼 특정 이벤트 객체의 상태를 파악하여 모든 작업을 제어한다. 따라서, 상대적으로 단순한 구조를 갖고 있다. 또한 ACL을 올바르게 설정하였다면 다른 스레드는 물론 프로세스에서도 해당 이벤트를 같은 이름으로 받을 수 있다. 다만, 프로세스 사이에서 통신이 이루어질 경우에는 이벤트 이름 형식을 정확히 정의해 주어야 한다. 가령 다음과 같이 하면 될 것이다. 


   HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS, FALSE, _T("MyEvent")); 

5.8 대기 타이머(Waitable Timer) 및 타이머 큐(Timer Queue) 

지금부터 언급하는 내용들은 근본적으로 위에서 언급된 루틴과는 다르다. 아예 Sleep()과 같은 함수와 동일한 개념으로 동작한다고 생각하면 된다. 즉, 일정 시간의 유예를 둔 뒤 해당 시간이 지나면 자동으로 해당 작업을 멈추어 버리는 방법이다. 타이머는 뮤텍스나 세마포어처럼 명시적으로 스레드 동기화에 관여하지 않으므로, 실제로는 동기 함수라기 보다는 특별한 경우에만 주로 사용된다. 이는 Sleep() 함수보다 더 시스템을 효율적으로 다룬다는 특징을 지닐 뿐이다. 따라서 Sleep()이 필요한 시점에서 대기 타이머나 타이머 큐를 사용하는 것을 권한다. 

타이머 큐는 단순히 일정 시간을 기다리는 콜백 함수이며, 대기 타이머는 단순히 기다리는 것이 아니라, 이벤트와 같이 ON/OFF 등을 구분하는 상태를 나타낼 수도 있고, 특정 시간이 지나면 주기적으로 활성화되는 형태를 나타내기도 한다. 대기 타이머 역시 이벤트와 비슷하게 수동 타이머(Manual Reset Timer)와 동기 타이머(Synchronization Timer)가 존재한다. 동기 타이머가 이벤트의 자동 타이머와 비슷한 구실을 하며, 그 외에 주기 타이머(Periodic Timer)가 있다. 타이머 역시 생성과 소멸은 비슷한 문법을 가지므로, CreateWaitableTimer() 함수로 생성된다. 문법은 거의 이벤트와 흡사하며, 동기화에서 그렇게 많이 사용되지 않으므로 더 자세한 내용을 원하는 이들은 MSDN에서 사용법을 참고할 것을 권한다.


 


5.9 TLS 

스레드에서 전역 변수를 사용할 경우 모든 스레드가 해당 변수를 건드리기 때문에, 다른 스레드가 전혀 동작하지 않는다고 하더라도, 컨텍스트 전환의 결과로 특정 데이터를 엉망으로 만들어 버릴 가능성이 높다. 이 경우 스레드 로컬 저장소를 사용하면 문제가 깔끔히 해결된다. 스레드 로컬 저장 영역은 32비트 블록으로 지정되며, 각각의 스레드별로 사용 공간을 할당할 수 있다. 포인터로 값을 전달하기 좋아하는 C 계열의 프로그래머들이 일반적인 멀티스레드 프로그램을 작성할 때, 스레드와 해당 스레드가 호출하는 함수 사이에서 인수 전달이 포인터로 이루어진다면 여러 문제가 발생할 수 있다. 

제일 큰 문제는 포인터가 전역 변수로 지정되어야 한다는 것이다. 이 말은 컨텍스트 스위칭이 일어나지 않더라도, 각각의 멀티스레드 프로그램이 포인터로 지정된 변수를 일그러뜨릴 가능성이 매우 높다는 것이다. 가령, 항상 초기값이 0으로 세팅되어 있어야 특정 스레드와 그 연관 함수가 만족하지만, 스레드 수행 결과 그 포인터 변수는 초기값이 스레드의 결과값으로 치환된 이후 컨텍스트 전환이 일어난 경우 다른 스레드는 엉뚱한 초기값을 받게 된다. 물론, 이러한 부분은 뮤텍스나 세마포어 등을 사용하여 초기값을 항상 원래대로 복구하는 것으로 해결할 수 있다. 

그렇지만, 뮤텍스나 세마포어는 비교적 수행 속도가 느리고, 경쟁 상태나 교착 상태의 가능성이 없이 단순히 변수의 변화를 막기 위해 뮤텍스나 세마포어를 사용하는 것은 비효율적이다. 또한 스레드 동기화는 근본적으로 컨텍스트 전환은 계속 수행하고 있는 상황이기 때문에 CPU의 시간을 계속 소모하면서 대기하는 방법을 사용하게 된다. 따라서, 스레드와 그 스레드가 호출하는 함수 사이에서만 사용할 수 있는 포인터 변수를 도입하면 메모리 측면에서는 모든 참조형 변수의 사본을 만드는 손해를 볼 수 있지만, 각각의 스레드를 병렬적으로 수행하면서 컨텍스트 전환에 의해 발생하는 문제들을 근원적으로 해결할 수 있다. 경쟁 상태가 나타나지 않는, 단순히 멀티스레드의 이점을 최대한 활용하기 위해 이러한 방법론을 사용한다. 물론 저장 영역을 손해보고, 메모리 연산을 빈번히 사용하기 때문에 복잡해지는 경우 오류 가능성이 더욱 커질 수 있다. 

TLS는 각 스레드를 위한 전용 주소 공간을 제공하며, 각 주소 공간의 인덱스로 해당 공간을 접근한다. 이는 마치 지하철 물품보관소와 같이 각자가 물품보관소 열쇠를 갖고 있고, 해당 열쇠를 가진 사람만이 물건을 넣고 꺼낼 수 있는 형태로 운영된다. 각 스레드는 호출되는 함수를 통해 자신이 삽입한 변수를 공유할 수 있다. TLS는 정적, 혹은 동적으로 할당될 수 있으며, 마치 일반적인 C++ 프로그램의 메모리 할당과 비슷한 방법으로 동작하게 된다. 다음 그림은 TLS와 스레드의 관계를 나타낸 것이다. 


 


각각의 스레드는 TLS 영역에 보관함을 가질 수 있으며, 이 스레드는 자신이 호출한 함수, 혹은 자신 내에서만 이 TLS 영역에 접근할 수 있다. 빈번히 사용되는 동적 TLS는 TlsAlloc() 함수로 저장 공간을 설정하며, TlsFree() 함수로 저장 공간을 해제한다. 저장 공간에 값을 입력할 때는 TlsSetValue() 함수로 값을 입력하고, 출력은 TlsGetValue() 함수로 처리한다.


다음 코드는 TLS를 동적으로 사용한 예제이다. 






#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; dwIndex=TlsAlloc(); //Tls 영역 설정 for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); TlsFree(dwIndex); //Tls 영역 해제 return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; int *m=new int; *m=j+1; TlsSetValue(dwIndex, (LPVOID)m); //변수를 TLS Index를 받고 TLS 영역에 삽입한다. CommonFunc(); return 0; } // 스레드와 함수 사이에서는 *m을 공유하지만, 이는 각각 스레드별로 독립적으로 행동한다. VOID CommonFunc(VOID) { int i, *m, k; m=(int *)TlsGetValue(dwIndex); //호출된 for(i=0;i<*m;i++) k=*m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 코드는 스레드 동기화를 위해 어떠한 방법론도 사용하지 않고 있다. 출력 화면에서 경쟁 상태가 나타나기는 하겠지만, 실제로 프로그램의 수행 결과는 예측대로 나타나게 된다. 

정적 TLS는 조금 더 다루기 쉽다. 동적 TLS가 전역 메모리 할당과 흡사하게 동작한다고 하면 정적 TLS는 전역 변수 선언과 비슷하게 선언된다. 앞선 동적 할당이 주로 포인터 변수를 처리하기 위한 방법으로 사용된다면, 정적 TLS는 전역변수가 항상 동일한 초기값을 갖도록 처리할 수 있다. 정적 TLS를 사용하는 방법은 단지 전역 변수를 선언할 때 __declspec(thread)키워드를 사용하면 된다. 다음 코드를 한 번 살펴보자. 






#include <windows.h> #include <iostream> #include <cstdio> using namespace std; DWORD WINAPI myfunction(LPVOID); VOID CommonFunc(VOID); DWORD dwIndex; __declspec(thread) int m=1; // TLS 전역 변수 사용 int main(){ HANDLE hThread[5]; DWORD threadId = NULL; BOOL bOK; int j; for(j=0;j<5;j++){ hThread[j] = CreateThread(NULL, 0, &myfunction, (LPVOID)j, 0,&threadId); if(hThread[j] == NULL) { cerr << "스레드 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } WaitForMultipleObjects(5, hThread, TRUE, INFINITE); bOK=CloseHandle(hThread); return 0; } DWORD WINAPI myfunction(LPVOID p){ int j; j=(int)p; CommonFunc(); m=m+j; // 만약 일반적인 전역 변수라면 이 구문이 호출된 이후 변수 자체가 변할 것이다. return 0; } // 스레드와 함수, 그리고 전체 코드는 m을 공유하지만, m은 생성된 스레드에 따라 다른 변수로 // 취급받는다. VOID CommonFunc(VOID) { int i, k; for(i=0;i<m;i++) k=m+i; cout << "k=" << k << "(Thread:" << GetCurrentThreadId() << ")" << endl; } 

이 프로그램이 초기 선언된 m 값을 일반적인 변수로 선언했다면 스레드에서 m 값을 계속 변경시키기 때문에 전역 변수는 각각의 스레드마다 다른 초기값을 받게 되며, 출력 결과는 예측할 수 없도록 변할 것이다. 그러나, 정적 TLS 변수로 선언되었기 때문에 모든 변수는 스레드 내에서 같은 초기값을 갖게 된다. 

정적 TLS를 사용할 때 TLS 인덱스는 명시적으로 생성되거나 파괴되지 않는다. 스레드가 생성될 때 TLS 저장 영역이 생성되며, 스레드가 파괴될 때 같이 파괴된다. 주로 멀티스레드 프로그램을 수행하면서 정적 초기 변수가 변경될 가능성이 있지만 이러한 변수들을 제외하고는 교착 상태나 경쟁 상태가 벌어지지 않을 경우 TLS는 유용하다.


 


6.1 스레드 우선권 

윈도우 시스템은 스레드를 기준으로 다중 작업이 이루어지는 멀티스레딩 프로그램이라고 앞에서 언급하였다. 스레드는 각각 일정 시간의 실행 시분할을 갖게 되며, 일단 동시에 모든 요청 스레드를 다 처리할 수 없으므로, 순서대로 처리하는 스레드 큐(Thread Queue)가 형성되게 된다. 그런데, 이 큐가 한 줄로 늘어선 것이 아니라, 우선권이라는 개념을 가진 상태에서 병렬적인 큐로 형성되어 있다. 우선권은 숫자가 커지면 커질수록 우선적인 개념을 갖고 있고 이는 시스템이 빨리 처리해야 하는 스레드이다. 

가령, 은행 객장에 우대고객 전용 창구가 따로 있고, 아무리 은행 줄이 길어도 이 우대고객 전용 창구에 줄을 선 우대고객은 가장 먼저 은행일을 볼 수 있는 것과 같은 개념이다. 윈도우는 이 우선권이 31번까지 있다. 따라서, 스레드가 어떤 큐에 들어가느냐에 따라 처리되는 속도가 달라지게 된다. 다음 그림은 스레드가 각각의 큐에 들어가 있는 모습이다. 


 


우선권이 높은 큐를 할당받는 스레드는 주로 I/O 등이나 시스템과 관계된 중요한 시스템들이다. 엄밀하게 말해서 우선권이 높은 큐는 그만큼 많은 시간 할당을 받게 된다. 시분할 멀티태스킹이기 때문이다.


윈도우 시스템은 동적으로 우선권을 관리한다. 이는, 프로그램이 실행되는 동안에도 계속 우선권이 변경될 수 있으며, 시스템은 항상 우선권을 적절한 방법으로 통제하고 관리한다. 가령, 윈도우 NT 이상의 시스템에서 나타나는 성능 옵션에서, 포그라운드 서비스와 백그라운드 서비스의 응용 프로그램 응답 옵션을 바꿀 수 있다. 참고로 윈도우 시스템은 유닉스 시스템과 포그라운드 서비스와 백그라운드 서비스의 개념상의 차이가 존재한다. 윈도우에서는 로그온 여부와 프로세스의 생존이 바로 직결되어 있으므로 로그오프 이후에도 운영되는 프로그램은 존재하지 않는다. 어떤 방법이든 사용자 로그온이 처리되어 있어야 프로세스가 생존할 수 있으며, 이렇게 따진다면 유닉스 개념의 포그라운드와 백그라운드가 나타나지 않는다. 

윈도우 시스템에서 백그라운드 서비스는 현재 활성화되지 않은 모든 다른 프로세스를 의미한다. 즉, 로컬 로그온이 처리되어 창의 상단의 바가 파란색으로 바뀌지 않은 다른 모든 프로세스는 백그라운드 서비스이다. 이 경우, 성능 옵션에서의 다음 조정은 결국 우선권의 조정일 수밖에 없다. 


 


실제로 위와 같은 성능 옵션의 응용프로그램 최적화는 원래 이 응용프로그램이 갖도록 정의되었던 스레드의 우선권보다 다소 높은 우선권을 동적으로 설정하라는 것이다. 우선권의 개념은 작업 관리자에서도 볼 수 있으며, 작업 관리자에서는 우선권 클래스에 해당하는 다섯 가지 개념이 나타나게 된다. 이 우선권들은 일차적으로 프로세스가 형성될 경우 지정된다. IDLE(4), NORMAL(7,9 또는 7+1=8, 9+1=10), HIGH(13), REALTIME(24)가 일차적으로 부여되며, 이 값에 가감을 하여 우선권 적용이 이루어진다. 가감은 +4~-3까지 가능하며, 이 조합으로 전체 우선권을 만들어 낼 수 있다. 따라서 우선권은 총 32개가 있기는 하지만 여기서 만들어 낼 수 있는 것은 총 21개가 된다. 이 중 가감이 될 수 없는 절대값(1, 15, 31)이 존재하며, 포그라운드 성능 최적화를 한 경우 초기부터 9-10 사이의 우선권이 주어진 채로 출발하게 된다. 음영을 넣은 부분은 실시간(Real Time) 우선권에 해당하는 부분이며, 일반적으로 사용자 응용프로그램의 우선권이 15 이상으로 올라가는 경우는 거의 없다. 만일 사용자 프로그램의 우선권이 15 이상으로 올라간다면 이는 프로그램상의 버그이거나 프로그래머가 의도적으로 실시간 큐에 삽입한 것이며, 이런 상황에서 프로그램의 응답이 지연되는 경우 전체 시스템이 같이 느려지는 현상이 벌어진다. 이는 상위 큐에 시간 할당이 더 많이 되어 있으며, 별다른 이유 없이(콜백 함수나 객체 상태 함수를 사용하지 않고) 시스템이 느려진다면 CPU 자원을 계속 잡아먹고 있는 셈이 된다. 15 미만에서 시작한 스레드의 우선권 상한도는 15까지이며, 15 이상에서 시작한 우선권이 상한선 31을 갖게 된다. 실제로 이렇게 높은 우선권을 갖고 있는 경우는 찾기 어렵다. 윈도우 CE의 경우 우선권은 32가지를 모두 사용하며, 이러한 측면들은 윈도우 소프트웨어가 발전할수록 계속 변하는 부분이다. 다음 표는 윈도우 스레드 우선권을 전부 나열한 것이며, 어떤 우선권이 있으며, 어떻게 우선권 변경이 되는지 확인해 볼 수 있다. 15, 31 은 절대값이며, 더 이상 가감되지 않는다.



 


6.2 우선권 가감


스레드의 우선권 가감 연산은 누적하여 수행할 수 없다. 운영체제는 하나의 스레드에 대한 기저 우선권(프로세스가 시작되는 시점에서의 우선권, Base Priority)을 기억하고 있으며, 스레드의 우선권 조정 함수는 이 기저 우선권에 대해서만 가감을 한다. 즉, 원래 기저 우선권이 10인 스레드에 우선권 조정을 몇 번 반복하더라도 우선권은 12 또는 15로밖에는 설정할 수 없다는 것이다. 따라서, 우선권은 사용할 수 있는 범위 한계가 있다. 다음과 같이 프로그램을 작성하고 컴파일해 보자. 






#include <windows.h> #include <iostream> using namespace std; int main(){ HANDLE hThread; char a; hThread=GetCurrentThread(); cin >> a; // 잠시 정지를 위함 SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST); cin >> a; return 0; } 

이 프로그램은 현재 동작하는 스레드(주 스레드)로부터 핸들을 얻어, 자신 스레드의 우선 권을 변경하는 프로그램이다. GetCurrentThread() 함수는 현재 동작하는 스레드의 핸들을 반환하며, SetThreadPriority() 함수는 현재 스레드의 우선권을 변경한다. 초기 기저 우선권은 프로세스가 형성될 때 생성되며, 별다른 옵션 없이 프로세스를 생성하였다면 기저 우선권은 운영체제가 적절하게 정의한다. 

이 프로그램은 기저 우선권에 우선권을 계속 더하는 기능을 하고 있다. THREAD_PRIORITY_NORMAL은 +2를 하는 키워드이며, 이 프로그램을 수행하면서 성능 모니터를 같이 띄워 스레드의 우선권이 어떻게 변하는지 한 번 확인해 보기 바란다. 만일 기저 우선권이 8이었다면 10, 12, 14 이렇게 계속 증가할 것인지, 아니면 8+2 만 계속 반복하여 결과는 항상 10이 나오는지 측정해 보면 된다. 

각자의 컴퓨터는 모두 다 다른 설정을 지니므로, 기저 우선권은 1~2 정도 오차가 있을 수 있다. 따라서, 프로그램의 수행 결과 초기값에 비해 어느 정도의 변경이 있었는지를 알아내는 것이 중요하며, 컴파일해서 실행해보면 알겠지만, 기저 우선권에 비해 결코 2 이상의 값이 올라가지 않는다. 시스템은 해당 스레드의 기저 우선권을 기억하고 있고, 해당 명령을 계속 수행하면 기저 우선권에 계속 2를 더하게 되는 까닭이다. 

우선권의 변경은 일반적으로는 운영체제의 필요에 의해 이루어진다. 이 말은 운영체제의 필요가 아닌 경우라면 우선권의 변경은 프로그램적으로는 거의 이루어지지 않는다는 것이다. 간혹 인터넷 익스플로러 등의 프로그램이 작업 관리자의 종료 명령도 무시하고 계속 수행되는 경우를 볼 수 있으며, 이런 현상은 프로그램의 버그 등에 의해 작업 관리자 보다 해당 프로그램의 스레드 우선권이 높은 경우에 나타나게 된다.


 


7.1 윈도우와 프로세스 

유닉스에서는 여러 개의 프로세스를 동시에 운영하는 것이 보편적이었다. 분기(fork)라는 형태로 하나의 프로세스에서는 다른 프로세스를 형성하고, 그 기저에는 셸 프로세스가 존재한다. 일차적으로 셸 프로세스가 생성되고 그 위에서 프로세스는 계속 시스템 분기를 사용하여 프로세스를 형성해 나간다. 만들어진 프로세스는 이전 프로세스의 거의 대부분의 속성을 계승하며, 특히 보안성과 관련되어서는 출발한 프로세스의 대부분의 속성을 갖고 있다. 

프로세스는 해당 프로그램에서 필요한 주소 공간을 확보하고 있으며, 관계 리소스를 모두 보유하고 있다. 유닉스에서는 fork() 함수를 사용하여 시스템 호출로 새로운 프로세스를 만들어 내며, 프로세스 자체가 차지하는 오버헤드가 그렇게 크지 않기 때문에 멀티프로세스 프로그램이 힘들지 않게 운영된다. 

윈도우 시스템에서는 런타임 라이브러리 레벨의 _spawn() 혹은 _exec() 함수를 사용하거나, Win32 API 레벨의 CreateProcess() 함수를 사용할 수 있다. CreateProcess() 함수는 상당히 많은 인수를 갖고 있으며 그 대부분은 프로세스에 대한 미세 조정이다. 

주의할 점은 윈도우에서는 프로세스를 여러 개 생성시키는 형태의 작업은 거의 이루어지지 않으며, 필요에 의해 프로세스를 생성한다고 할지라도 최소한의 프로세스 작업으로 처리해 주어야 한다. 윈도우 프로세스는 새로 프로세스를 형성하였을 경우에는 기존 프로세스와 완전히 별개인 하나의 프로세스가 형성된다. 실제로 윈도우 시스템에서 새로운 프로세스를 형성하여 작업을 시도하는 경우는 그렇게 많지 않다. 동시에 많은 창이 뜨는 윈도우 프로그램은 거의 다 새로운 창은 하나의 스레드로 동작하도록 설정한다. 프로세스와 셸을 가득 띄워 놓은 유닉스 프로그램에 익숙하다면 이렇게 동작하는 방법론이 다소 답답할 수도 있을 것이다. 

작업 관리자를 사용하면 현재 운영되고 있는 프로세스와 프로세스가 사용하고 있는 CPU의 시간 점유율을 볼 수 있다. 프로세스가 사용하는 시간 점유율은 그 프로세스가 형성한 모든 스레드의 동작 시간의 총합으로 정의된다. 스레드를 전혀 생성하지 않도록 프로그램을 설계했다고 하더라도 최소 하나의 스레드가 정의된다고 앞에서 이미 언급하였다. 

실제로 프로세스를 생성하는 것은 아주 다양하고 복잡한 개념을 포괄하고 있다. 기본적으로 윈도우 2000은 보안성이 강한 운영체제이며, 이에 따라 어떤 스테이션, 어떤 세션, 그리고 어떤 ACL을 갖고 있는지 프로세스 자체에 명시되어야 한다. 일상적으로 이러한 작업들은 유닉스와 비슷하게 모 프로세스의 사본을 사용하는 경우가 많지만, 윈도우에서는 근본적으로 이런 설정들을 아예 처음부터 “새로” 만들 수 있다는 사실을 주목하자. 프로세스와 스레드 자체는 윈도우 보안성과 아주 밀접히 관련되어 있으며, 이런 일들은 시스템에 누가 들어와 있는지 판별하는 과정과 아주 밀접하게 관련된다. 

7.2 프로세스 생성 



프로세스 생성을 위해서는 BOOL 형의 CreateProcess() 함수가 사용된다. 이 함수는 생각보다 인수가 많으며, 각각의 인수들 중 일부는 구조체로 정의되어 있어 전부를 이해하는 것이 그렇게 생각보다 쉽지는 않다. 또한 보안성이라는 것이 같이 맞물려 있어, 해당 프로세스를 어떻게 형성할 것인지에 대해 정의할 수 있다.


윈도우는 다양한 종류의 프로세스가 존재한다. 여기서 Win32 API에 한정한다면 16비트 코드를 해석할 수 있는 WOW(Windows On Win32) VDM(Virtual DOS Machine) 내에 프로세스를 한정시킬 것인지, WOW VDM도 공유 VDM을 쓸 것인지 전용 VDM을 쓸 것인지 고려해야 하는 문제가 벌어진다. 하나의 프로세스에서 다른 프로세스를 형성할 때 해당 프로세스가 혹시 Win16 코드를 갖고 있지 않는지 한 번쯤 살펴볼 필요가 있다. 윈도우 XP가 나오는 이 시점에서 Win16 코드를 논하는 것이 시대착오적인 것 같지만 실제로 중소규모 소프트웨어 회사에서 제작한 아주 많은 애플리케이션은 여전히 Win16 코드를 일부 갖고 있다. 

다음은 아주 간단한 프로세스 생성의 예이다. 이 코드는 인터넷 익스플로러를 실행시키고 모 프로세스는 중단된다. 






#include <windows.h> #include <iostream> using namespace std; int main(){ STARTUPINFO stInfo; PROCESS_INFORMATION pInfo; BOOL bOk; LPTSTR strPath="C:\\Program Files\\Internet Explorer\\iexplore.exe"; ZeroMemory(&pInfo, sizeof(PROCESS_INFORMATION)); ZeroMemory(&stInfo, sizeof(STARTUPINFO)); stInfo.cb=sizeof(STARTUPINFO); bOk = CreateProcess(0, strPath, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &stInfo, &pInfo); if(!bOk){ cout << "프로세스 생성 오류:" << GetLastError() << endl; return -1; } WaitForSingleObject(pInfo.hProcess, INFINITE); // 생성한 프로세스가 종료될 때까지 기다린다. CloseHandle(pInfo.hProcess); CloseHandle(pInfo.hThread); return 0; } 

CreateProcess() 함수는 꽤 많은 인수를 갖고 있으며, MSDN에 이에 대해서는 아주 상세히 설명되어 있으므로 MSDN을 참조하기 바란다. 프로세스를 생성하는 것은 크게 네 가지 중요 정보를 제공해 준다. 일단 CreateProcess()가 받아들이는 인수는 다음과 같다. 

BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

보안 옵션은 사용자그룹 보안 분과에서 해당 내용을 찾아볼 수 있을 것이다. 생성 플래그는 몇 가지 방법론을 제공하며, 이 플래그를 어떻게 조작하느냐에 따라 16비트 VDM 혹은 디버그 이벤트 받음 여부 등을 설정할 수 있다. 또한 생성된 프로세스의 주 스레드를 대기 모드(suspended state)로 생성할 수도 있다. 프로세스를 생성하기 위한 환경 변수로 STARTUPINFO 구조체를 사용하며, 프로세스가 생성한 스레드 및 프로세스의 핸들을 받기 위해서 PROCESS_INFORMATION 구조체를 사용하게 된다. 환경 변수는 초기 실행이 고정되지 않은 프로세스를 대상으로 프로세스의 시작 위치, 윈도우 크기 등을 지정할 수 있으며, 프로세스 정보 구조체에서는 생성된 프로세스와 주 스레드의 ID 및 핸들을 얻을 수 있다. 

뒤에서 다루겠지만, 윈도우에서 프로세스가 새 프로세스를 생성할 때는 기존 프로세스의 속성을 계승할 수 있도록 설정하기도 하고, 계승하지 않도록 설정하기도 한다. 다음 제시되는 함수는 CreateProcess() 함수와 생긴 모양은 흡사하지만, 기존 프로세스와는 다른 사용자 계정에서 운영되는 새 프로세스를 형성한다. 이것은 4장에서 상세히 다루어질 것이다. 

BOOL CreateProcessAsUser( HANDLE hToken, LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); 

7.3 프로세스 종료 및 정보 얻기


유닉스에서는 전통적으로 kill 명령어를 사용하여 프로세스를 강제로 종료하였다. 윈도우 시스템에는 작업 관리자가 존재하지만 작업 관리자는 반드시 GUI로 실행해 주어야만 되는 단점이 존재하고 있다. 그런데 윈도우에서 프로세스를 종료하는 것은 그다지 어려운 작업이 아니다. 

프로세스 역시 커널 객체이므로, 이제 커널 객체에 익숙해졌다면 생성된 프로세스 ID로 프로세스 핸들을 받아 와서 해당 핸들을 가진 프로세스를 종료하면 된다는 등식이 머릿속에 도식적으로 나타날 것이다. 

그런데 윈도우 프로세스는 일반 객체가 아니라 보안성까지 체크되는 보안 객체이다. 이러한 까닭에 단순히 프로세스 번호로 해당 프로세스를 종료시키는 것이 아니라, 해당 프로세스의 ACL 혹은 시스템 프로세스 여부 등을 파악하여 최종적으로 종료시킬 수 있는지를 판단한다. 사용자가 충분한 권한이 있으며, 해당 프로세스를 종료시킬 수 있다면 얻어낸 핸들값을 사용하여 프로세스를 완전히 종료시킬 수 있다. 

시스템 프로세스의 경우 작업 관리자를 통해서도 액세스 거부가 나타나며, 이는 보안 분과에서 이유를 알아보도록 하라. 작업 관리자 등을 사용하여 알아낸 특정 프로세스를 종료하기 위한 코드는 다음과 같다. 






#include <windows.h> #include <iostream> using namespace std; int main(int argc, char* argv[]){ if (argc !=2) { cout << "사용법: kill Pid" << endl; return -1; } int processId=atoi(argv[1]); unsigned uExitCode=0; HANDLE hProcess; hProcess=OpenProcess(PROCESS_TERMINATE, FALSE, processId); if(TerminateProcess(hProcess, uExitCode)) cout << "프로세스 " << processId << "번을 종료했습니다." << endl; else cout << "종료하지 못했습니다." << endl; return 0; } 

파일이나 기타 핸들을 사용하는 여러 구문들은 전부 이와 비슷한 형태로 동작하게 된다. 만일 프로세스를 종료하지 못했다면 프로세스가 시스템에 소속된 것이거나, ACL이 맞지 않아 종료할 수 없는 것일 가능성이 높다. 

프로세스를 중단하기 위해서 TernminateProcess() 함수를 주로 사용하지만, 간혹 프로세스가 자살할 필요가 있다. 보편적으로 윈도우 프로그램은 명시적으로 프로세스를 종료하라는 말이 없을 경우에는 프로세스를 종료하지 않으므로 ExitProcess() 함수를 사용하여 종료할 수 있다. 이 함수는 자살을 위해 사용하므로 void 형이면 충분하다. 

ExitProcess() 함수는 사용중인 DLL에 대한 참조값을 제거하고 발생시킨 모든 스레드의 참조값 역시 제거한다. 그리고 스스로 죽고 싶다는 메시지를 객체 관리자에게 송신한다. 그러나, 객체 관리자가 “죽여” 주기 전까지는 실제로 프로세스 객체가 사라지는 것은 아니다. 이는 앞에서 언급하였다.


 


8.1 파이버 

파이버는 스레드보다 더 작은 단위의 일을 의미한다. 보통 SQL 서버를 운영하면서 파이버라는 단어를 처음 볼 수 있다. 다음과 같은 정보 창에서 윈도우 파이버를 이용하는 체크박스를 한 번쯤 보았을 것이다. 


 


멀티 스레딩 프로그램에서 동시에 엄청난 다중 작업이 들어왔을 경우, 시스템은 컨텍스트 전환 오버헤드가 걸리게 된다. 특히 다중 프로세서를 운영하는 중앙 데이터 서버와 같은 경우는 초당 수천 건의 컨텍스트 전환과 100%에 육박하는 CPU 점유율을 보이는 경우가 다반사이다. 이러한 경우에 특화된 해결책이 파이버이다.


스레드(Thread)가 실을 의미하고 있기 때문에 실의 원료가 되는 섬유(fiber)로 이름을 붙인 것으로 생각된다. 파이버는 스레드와 달리 컨텍스트 전환을 시스템의 스케쥴에 따르지 않고 수동으로 해 주어야 한다. 따라서, 컨텍스트 전환에 따른 오버헤드가 발생하지 않는다. 그런데, 컨텍스트 전환을 수동으로 하는 것은 결국 멀티스레딩의 강점을 상당 부분 잃어버리는 것을 의미한다. 따라서, 파이버로 모든 코드를 작성하는 경우는 존재할 수 없다. 이는 프로세스 단위에서 항상 싱글스레딩을 영위하고 있는 결과를 낳게 된다. 

만일 파이버가 스레드에서 파생될 수 있다면 어떤 의미를 지닐까? 이는 컨텍스트 전환은 스레드 단위에서 이루어지고, 업무는 파이버 단위에서 이루어지는 것을 의미한다. 가령 100개의 스레드를 사용하는 응용프로그램이 존재한다고 할 때, 스레드 100개를 영위하기 위해서는 100가지의 서로 다른 상태를 저장하고, 이들 사이에서 전환이 이루어져야 하지만, 50개의 스레드와 2개의 파이버를 사용한다면 컨텍스트 전환에 따르는 오버헤드를 다소 줄일 수 있다 시스템에서 이루어지는 컨텍스트 전환은 스레드 단위이기 때문에 각각의 스레드는 어차피 할 컨텍스트 전환이 반으로 줄어드는 셈이 된다. 그리고, 파이버 상호간에 이루어지는 컨텍스트 전환은 그 오버헤드도 작고 수동으로 이루어지므로 많은 장점이 존재한다. 

그러나 스레드 대신 파이버를 사용하는 것은 성능 계산을 정확하게 해야 하는 단점이 존재한다. 따라서, 대용량 멀티스레드 프로그램에서 주로 채용되며, 현재까지 가장 효과적으로 사용되고 있는 것은 멀티프로세서가 장착된 MS-SQL 데이터베이스 서비스이다. 파이버를 사용할 경우 가장 큰 약점은 자기 자신의 프로세스가 전체 CPU를 대부분 장악하는 상황이 아니라면 컨텍스트 스위칭에서 후순위로 밀려버리는 상황이 벌어질 수 있다는 것이다. 즉, 스레드 숫자가 적기 때문에 할당에 따른 상대적 차별을 당할 수 있다. 따라서, SQL Server의 경우 CPU 점유량이 많지 않다면 파이버로 스레딩을 대신하는 것은 아무런 득이 되지 못할 가능성이 높다. 

다음은 파이버를 사용하는 예제 코드이다. 






#define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream> using namespace std; void WINAPI myfunction(LPVOID); int i=100; LPVOID LPFiber[5]; int main(){ int j; for(j=0;j<5;j++){ LPFiber[j] = CreateFiber(16384, &myfunction, (PVOID)j); if(LPFiber[j] == NULL) { cerr << "파이버 생성을 하지 못했습니다. :" << GetLastError() << endl; return -1; } } ConvertThreadToFiber(0); //주 스레드를 파이버로 변신시킨다. SwitchToFiber(LPFiber[0]); // 첫번째 파이버로 컨텍스트 전환을 시도한다. for(j=0;j<5;j++){ DeleteFiber(LPFiber[i]); //파이버를 지워 할당된 자원을 해제한다 } return 0; } void WINAPI myfunction(LPVOID p){ int j,k; j=(int)p; for(k=0;k<i;k++){ cout << k << endl; } if(j < 4) SwitchToFiber(LPFiber[j+1]); } 

파이버는 컨텍스트 전환에 따른 동기 문제가 발생하지 않기 때문에 동기 관계 객체들이나 이벤트 등을 사용할 필요가 전혀 없다. 다만 생성된 파이버 상호간 수동으로 전환하는 코드를 삽입하여야 한다. 스레드가 DWORD형의 함수를 갖는 데 반해 파이버는 VOID 형이라는 차이가 있다. 일단 전역 파이버를 선언한다. 그리고, 각각의 파이버를 생성한다. 다른 객체와 마찬가지로 파이버를 생성하는 것도 CreateObject 형식의 CreateFiber() 함수를 사용한다. CreateFiber()함수는 파이버에 할당될 초기 스택 크기와 함수 포인터, 그리고 전달 인수를 받아들인다. 파이버 당 할당될 수 있는 초기 최대 스택 크기는 따로 정의하지 않는 한 1MB이며, 이 인수를 제외하고는 스레드를 사용하는 것과 큰 차이가 나지 않는다. 

파이버는 상호 컨텍스트 전환을 수동으로 해 주어야 하며 이 경우 주 스레드 역시 파이버로 변경되어야 컨텍스트 전환이 가능할 것이다. 따라서, 주 스레드를 파이버로 변경하는 ConvertThreadToFiber() 함수를 따로 제공하고 있다. 이 함수를 호출하게 되면 주 스레드가 파이버로 인식되며 (사실은 모든 파이버를 포함한 전체 주 스레드가 하나의 스레드로 시스템 상에서는 간주되는 것이지만) 파이버 상호간 컨텍스트를 수동으로 전환하는 SwitchToFiber() 함수를 사용하여 다른 파이버로 실행 권한을 넘기게 된다. 이 경우 제어권을 넘길 파이버는 어떤 파이버로 자신의 제어권을 넘길 것인지를 명시해 주어야 하며, 선언된 VOID형의 파이버 이름을 인수로 전달한다. 

위 프로그램을 컴파일하여 실행하면 지금까지 보았던 예와 동일한 결과를 얻을 수 있을 것이다. 그렇지만 파이버로 작성한 프로그램은 프로세스 내에서 스케쥴 퀀텀을 받을 수 있는 기회가 줄어든다는 것을 잊어서는 안 된다. 

8.2 작업 객체 

작업 객체는 프로세스와 스레드를 다루는 객체 단위 중 가장 큰 것으로, 일련의 프로세스를 하나의 작업(job)으로 묶어 놓은 것이다.

반응형

+ Recent posts