=======================
=======================
=======================
출처: http://pkss.tistory.com/entry/CreateEvent-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
공부는 역시 상황에 쫓겨서 하는게 가장 효과적이라는.. (..응? 좀 미리 미리 해놓지!!!!)
CreateEvent
(Microsoft Windows CE 3.0)
The CreateEvent function creates a named or an unnamed event object. : 이벤트를 만드는 함수랍니다.
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPTSTR lpName);
Parameters
lpEventAttributes : Ignored. Must be NULL. 무시하라는데여.bManualReset : a manual-reset 이냐 auto-reset event object 이냐를 결정한답니다.bInitialState : 이벤트 obj의 초기 상태를 결정. 그니까 FALSE를 해야 만들었다가 나중에 set signal 하죠.lpName : 이벤트 이름Return 값 : 핸들값이 나옵니다.
그럼 이 함수의 용도와 용법을 알아보면..
CreateEvent(), SetEvnet(), ResetEvent()
1. 함수의 용도
WaitForSingleObject(), WaitForMultipleObjects() 등을 위한 Event Object를 만들고, 그것을 signal 상태 혹은 nonsignal 상태로 만들기 위해 사용됩니다.
2. 기본 용법
1) Event Object 를 생성한다.
HANDLE gv_hEvent;
int gv_operationType;
h = CreateEvent(NULL, TRUE, FALSE, NULL);
2) Thread Function을 만든다.
DWORD WINAPI MyThread(LPVOID lParam)
{
DWORD ret;
while (TRUE)
{
//gv_hEvent 가 set될 때까지 무한정 기다린다.
ret = WaiForSingleObject(gv_hEvent, INFINITE);
if(ret = WAIT_FAIL)
return 0;
}
else if ( ret = WAIT_ABANDONED) {
ResetEvent(gv_hEvent);
continue;
}
else if ( ret = WAIT_TIMEOUT) {
continue;
}
else {
if(gv_operationType==0)
return 0;
//필요한 작업을 하세요
ResetEvent(gv_hEvent);
}
}
}
3) UI 코드에서 Thread 가 작업을 하게 하고 싶다면
gv_operationType = 1;
SetEvent(gv_hEvent);
// --> 이 함수로 인해 gv_hEvent 가 signal 상태로 바뀌고, Thread의 WaitForSingleObject 가 return 된다.
** 참고로,
CreateEvent() 요놈은 네개의 인자를 가지는데,
첫번째는 생성된 핸들을 자식 프로세스가 상속받도록 하겠느냐
두번째는 이벤트를 자동으로 리셋시킬것이냐 아니냐
세번째는 초기값이 시그널이냐 아니냐
네번째는 이벤트의 이름을 스트링으로 주는것
WaitForSingleObject () 에서 이벤트를 기다리다가,
이벤트가 시그널되면 요놈이 리턴할텐데
그러고 나서 이벤트가 자동으로 리셋되기도하고, 변화가 없기도 하다.
그걸 결정하는것이 이벤트를 만들때 CreateEvent() 의 두번째 인자이다.
TRUE 면, 변화가 없고,
FALSE면, 자동으로 Reset된다.
모르고 쓰다가 낭패당하기 쉽상 ^^
예)
HANDLE h = CreateEvent(0,FALSE, TRUE,0); // 1번 요렇게 해보고
HANDLE h = CreateEvent(0,TRUE, TRUE,0); // 2번 요렇게도 해봐라
for(int i = 0; i<5; i++){
WaitForSingleObject(h, 1000);
printf("%d\n",i);
}
결과는 FALSE 인 1번의 경우 자동으로 리셋되기땜시롱 매번 1초씩 있다가 출력.
TRUE 인 2번의 경우 리셋되지 않기때문에 항상 시그널 상태이고, 기다릴것없이 두루룩~~
0,1,2,3,4 를 출력할것이다.
출처: http://pkss.tistory.com/entry/CreateEvent-사용하기 [pkss]
=======================
=======================
=======================
1. Process와 Thread
쓰레드라는 놈을 이해하기 위해선 프로세스란 놈을 먼저 이해해야한다. 프로세스는 EXE란 확장명을 가지고 파일을 실행시켰을 때 실질적으로 컴퓨터의 메모리에 자리잡고 앉아서 실행되는 형태를 말한다. 예를 들면 프로그램을 마우스로 더블클릭하면 OS가 그 프로그램을 실행하기 위해, 알맞게 메모리상에 그 프로세스가 실행가능한 영역을 잡아주고(이런 영역에는 여러가지 영역이 있다.코드부 or 스택부), 프로그램은 거기에 들어앉아서 온갖 사용자의 명령을 다 받으며 자기의 일을 수행한다.
즉, 프로세스는 메모리에 적재된 상태로 동작하는 프로그램이다.
프로세스를 이루는 기본단위가 쓰레드라 볼 수 있다. 프로세스는 적게는 수십개, 많게는 수백개의 Thread로 이루어져 있다. 즉 이런 쓰레드들이 실처럼 일률적인 순서대로 늘어져 원하는 작업을 수행하고 있는 것이죠. 쉽게 보면 쓰레드 하나가 특정한 작업을 하나 수행한다고 생각 할 수 있다.
프로그램을 짜다보면 여러가지 상황이 발생하는데, 블록킹모드, 논블록킹 모드, 동기모드, 비동기모드 등등..
예를 하나 들어 보면,
while( true )
{
int count;
printf( “Current number %d”, count );
count++;
}
위의 루틴에 들어가게 되면 프로그램은 블록(무한 Loop에 빠짐)됩니다. 사용자는 어떤 입력도 프로그램에 가 할 수 없습니다. 그래서 사람들은 저런 반복적으로 작업하는 계산 루틴 등을 쓰레드로 만들어 버린다. 즉 계산과정을 쓰레드로 만들어서 하나의 작은 작업으로 처리해 버리는 것이다. 그러므로 저렇게 반복적인 작업이나 시간이 오래 걸리는 작업, 또는 다른 작업과 병행해서 일을 해야 하는 경우는 쓰레드를 사용하게 된다.
물론 처리 속도 문제는 조금 다른 문제이다. 단지 쓰레드로 프로그램을 작성하면, OS차원에서 쓰레드가 CPU를 사용할 시간을 분배(Schedule)하는 것뿐이다. 단지 이 시간의 간격이 무지 짧기 때문에 사용자는 느끼지 못하고, 마치 순간적으로 동시에 작동하는 것처럼 보인다.
쓰레드는 사용자에게 융통성 있는 사용환경을 만들어 주기 위해서 개발자가 충분히 검토하고 개발에 적용해야할 테크닉이다.
2. 쓰레드의 장점
A. 애플리케이션의 일부가 비동기적이고 병렬적으로 실행가능하게 함
B. 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에게 동일한 서비스 제공가능
C. 블록(죽어버릴 수 있는)될 가능성이 있는 작업을 수행할 때 방지할 수 있음
D. 멀티프로세서 시스템을 효과적으로 사용 할 수 있음
A. 애플리케이션의 일부가 비동기적이고 병렬적으로 실행가능하게 함
동기적이란 어떤 작업이 순차적으로 실행되어야 함을 말한다. 예를 들어 어떤 함수 두개가 있다고 가장하자.
int a = 2, b = 3, c;
int d = 4, e = 5, f;
funcA { c = a+b;}
funcB { f = d+e;}
단일 쓰레드 환경에서는 함수A를 호출한 다음에 함수B를 호출한다.
funcA()
funcB()
그런데 함수A와 함수B가 서로 완전히 다른 데이터를 가지고 작업을 한다면, 함수가 실행되는 순서에는 상관이 없다. 즉 함수의 호출 순서가 바뀌어도 상관이 없다.
funcB()
funcA()
그러나 새로운 함수 C를 만들어 보자.
in funcC { return c + f;}
위의 함수는 함수A와 함수B의 결과값을 가지고 연산을 하기 때문에, 함수A와 함수B가 완전히 실행이 끝난 다음에야 호출할 수가 있다. 위와 같이 순차적으로 실행을 해야만 하는 순차적인 프로그램 방식을 동기화(Synchronization)라고 합니다. 비동기화는 위와 같이 순차적으로 실행을 안 해도 되는 것이다.
단일 쓰레드를 지원하는 OS에서는 반드시 위와 같은 절차를 가지고 프로그래밍을 해야 하지만 멀티 쓰레드를 사용하는 OS에서는 프로그램을 작성할 때 각 부분 사이에 상호관계를 반영하여 설계를 할 수 있도록 하는 것이다.
B. 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에게 동일한 서비스 제공가능
Network 프로그램 같은 경우 사용자의 접속을 계속 처리하기 위해서 쓰레드를 사용합니다. 즉 사용자가 접속 요청을 하면 하나의 쓰레드와 연결시켜준다. 여러 사용자가 단일 쓰레드의 서버 프로그램에 접속해서 서비스를 받는다고 가정하면, 한 명이 접속해서 그 사람이 접속을 끊을 때까지 다른 사용자는 서비스를 받을 수 없다.
C. 블록(죽어버릴 수 있는)될 가능성이 있는 작업을 수행할 때 방지할 수 있음
Network 프로그래밍에서 블록 되는 코드가 있다고 가정하자. 대표적으로 보면 accept()가 있는데, 이 함수는 사용자가 접속요청을 할 때까지 멈춰 있는다. 사용자 접속요청이 없으면 무한정 기다리게 된다. 그래서 접속요청을 받는 부분을 쓰레드로 만들어서 프로그램이 블록되지 않게 하는 것이다.
또 다른 예로, 탐색기에서 덩치가 큰 파일을 복사를 걸어놓고도 곧바로 탐색기를 다시 있는데, 이는 복사하는 루틴을 쓰레드로 만들었기 때문이다.
D. 멀티 프로세서 시스템을 효과적으로 사용 할 수 있음
CPU가 많은 시스템에서 쓰레드를 돌리면 좀더 효율적이다.
3. 멀티쓰레드 애플리케이션 구조
멀티쓰레드를 이용한 애플리케이션을 작성하는 구조에는 3가지 방법이 있다.
A. boss/worker모델.
B. work crew모델.
C. 파이프라이닝 모델.
A. Boss/Worker Model
쓰레드(주쓰레드)가 필요에 따라 작업자 쓰레드를 만들어 내는 경우에, 위에서 예를 든 것과 같이 접속받는 부분을 주쓰레드(boss)로 돌리고, 접속요청이 오면 새로운 쓰레드(worker)를 만들어 사용자와 연결시켜 주는 방법입니다.
B. Work Crew Model
어떤 한 작업을 여러 개의 쓰레드가 나눠서 하는 방식이다. 예를 들어, 집을 청소하는 작업에 대한 쓰레드를 여러 개 돌려, 방바닥 닦는 사람, 먼지 터는 사람 이런 식이다. 특정한 청소를 하는 작업이 쓰레드 하나가 되는 것이다.
C. Pipe Lining Model
이 구조에서는 순서대로 실행되어야할 작업이 여러 개가 있을 경우, 작업 1은 작업 2에게 전달하고, 작업2는 작업3에게 전달하는 방식으로 순차적으로 진행되어야 하지만 최종결과를 여러 개 만들어내기 위해서 동시에 수행될 필요가 있을 경우 멀티쓰레딩을 사용한다. 예를 들면 공장의 라인에서는 서로 다른 작업들을 수행하지만, 결국 하나의 결과물을 만들어 내기 위한 과정이다.
4. 쓰레드의 생성
쓰레드는 UI( User Interface ) Thread와 Worker( 작업자 ) 쓰레드로 나뉜다.
UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는 쓰레드를 말한다.) 쓰레드이다.
Worker 쓰레드는 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수를 위한 것이다.
윈도우에서 쓰레드를 생성하는 방법은 여러가지가 있다. 즉 윈도우라는 OS에서 제공해주는 라이브러리함수를 가지고 쓰레드를 만드는 것입니다.
Wokrer 스레드를 만드는 방법을 알아보자.
어떤 프로그램이 Dialog 기반으로 돌아가며, 화면에 2초에 한번씩 숫자를 더하면 화면에 바뀐 숫자를 표시한다고 가정한다.
① 화면에 무한적으로 숫자를 뿌리는 함수를 만듦
UINT ThreadFunc( void* pParam )
{
int count;
while(true)
{
count++;
GetDlgItem( IDC_STATIC )‐>SetWindowText( atoi(count) );
Sleep( 2000 );
}
}
//정확한 코드는 아님
// IDC_STATIC는 static_control 의 resource id 임
이 함수는 2초에 한번씩 count를 증가시키면서 Dialog box에 증가된 값을 화면에 뿌린다. 이 함수는 쓰레드로 돌리려 한다.
② Worker 쓰레드 생성하기
작업자 쓰레드로 특정한 작업을 하는 사용자 정의 함수를 만들기 위해서, 윈도우에서는 여러가지 쓰레드 생성 함수를 제공해 준다.
1. CreateThread()
2. _beginthread(), _beginthreadex()
3. AfxBeginThread(), AfxBeginThreadEx()
1. CreateThread()
win32함수로써, 중요한 parameter만 살펴본다.
HANDLE handle;
Handle = CreateThread( Threadfunc(), Param );
첫번째 parameter는 위에서와 같이 사용자가 쓰레드로 돌려야할 작업함수를 써 주며, 두 번째 parameter는 작업함수에 parameter값으로 전달할 값이다. 이 parameter는 VOID*으로 되어 있기 때문에 4BYTE이내의 값은 어떤 값이든 들어갈수 있다. 대신 TYPE CASTING을 해주어야 합니다.(ex. (int) 3). 이 함수가 올바르게 실행이 되면 쓰레드에 대한 핸들을 반환하는데, 이 핸들을 가지고 쓰레드를 조작할 수 있게 된다.
대표적으로 쓰레드를 닫을 때 CloseHandle()함수를 사용해서 쓰레드 핸들을 넣어주고 쓰레드를 닫아 준다. 그러나 이 함수로 생성된 쓰레드를 닫을때는 ExitThread()면 된다.
2. _beginthread(), _beginthreadex()
CreateThread는 쓰레드에서 win32 API함수만 호출할 수 있다. 즉, 사용자가 어떤 작업을 하는 함수를 만들 때 CreateThread로 만들게 되면, 그 함수안에서는 win32 API만 사용할 수 있고, C함수나 MFC는 못쓴다.
그러나 _beginthread() 함수는 win32 API와 C 런타임 함수를 사용할 때 사용한다.
주의할 점은 이 함수도 쓰레드 핸들을 리턴하는데, 이 핸들을 가지고 쓰레드에 조작을 가 할 수 있다. 대신 이 함수를 사용하면 C 런타임 라이브러리가 핸들을 자동으로 닫으므로 이를 직접 닫을 필요는 없다. 하지만, _beginthreadex는 스레드 핸들을 직접 닫아야 합니다. 그리고 이 쓰레드를 닫을 때는 _endthread(), _endthreadex()를 사용하면 된다.
3. AfxBeginThread(), AfxBeginThreadEx()
실질적으로 가장 자주 사용하는 쓰레드 생성함수 이다. 이 함수를 이용하면 사용자 정의 함수내에서 MFC, win32 API, C 런타임 라이브러리등 여러가지 라이브러리 함수들을 전부 사용할 수 있다. 이 함수는 리턴값이 CwinThread* 형을 리턴하는데, 이 객체를 사용하면, 생성된 쓰레드에 여러가지 조작을 가할 수 있다. AfxEndThread()를 사용해서 종료 시킬 수 있다. 쓰레드가 종료되면 MFC는 쓰레드 핸들을 닫고 리턴값으로 받은 CwinThread*객체를 제거합니다.
5. MFC에서 작업자 쓰레드 생성하기..
MFC에서 쓰레드를 만드는 방법은 두 가지가 있다. CwinThread의 멤버 함수인 CreateThread를 사용하는 방법과 AfxBeginThread()를 사용하는 방법이다. 후자는 CwinThread객체를 만들 수 있다.
또한 MFC에서는 AfxBeginThread의 서로 다른 버전 두개를 정의 하고 있다. 하나는 작업자 쓰레드를 위한 것이고, 하나는 UI쓰레드를 위한 것인데, 이 두 가지 정의대한 소스코드는 Thrdcore.cpp에서 볼 수 있다.
CwinThread* pThread = AfxBeginThread( Threadfunc, &threadinfo );
▪ nProiority는 쓰레드의 우선순위를 지정한다. 기본적으로 THREAD_PRIORITY_NORMAL이 들어가는데 이는 평범한 순서로 작동시키겠다는 말이다. 이 우선순위는 나중에 리턴값으로 받은 CwinThread* 의 멤버 함수인 SetThreadPriority를 가지고 변경해 줄 수 있다.
▪ dwCreateFlags는 0 아니면 CREATE_SUSPEND이다. 기본값은 0이 들어가는데 0이면 바로 쓰레드가 실행되는 것이고 두 번째이면 쓰레드는 정지된 상태로 만들어지고 리턴값으로 받은 객체의 멤버함수인 ResumeThread를 호출하면 쓰레드를 시작한다.
▪ lpSecurityAttrs는 보안속성이다.
AfxBeginThread() 함수의 첫번째 인자로 들어갈 작업함수의 원형은 다음과 같다.
UINT ThreadFunc ( LPVOID pParam )
이 함수는 static 클래스 멤버 함수 이거나 클래스 외부에서 선언한 함수여야 한다.
즉, 어떤 함수를 클래스 멤버함수로 선언해서 쓰레드로 사용하려면 클래스내부에 함수 프로토타입 선언시 static로 선언해 주어야 한다는 건데, static 멤버 함수는 static 멤버 변수에만 접근 할 수 있기 때문에 제약이 많다.
인자값은 AfxBeginThread에서 두번째 인자로 들어갔던 32bit값의 파라미터 인데, 일반적으로 그냥 데이터형을 넘겨줄 수도 있고, 많은 데이터가 있을 때에는 구조체를 만들어서 포인터로 넘겨주어서 함수내부에서 다시 풀어서 사용한다.
쓰레드 함수 내부에서 만들어진 객체나 변수들은 쓰레드 자신의 내부 스택에 생성되기 때문에 다른 쓰레드에서 데이터를 변경하거나 하는 그런 문제들은 없다.
6. 쓰레드 멈추고 다시 실행하기
AfxBeginThread() 함수로 작업을 할 함수를 스레드로 돌리고 그 스레드가 올바르게 작동이 되면 CwinThread*형을 리턴값으로 받는다는 것을 알게 되었다. 이렇게 되면 우리는 이 실행시킨 스레드를 사용자가 멈추고 싶을 때 멈추게 할 수 있고, 또 멈췄으면 언젠가 다시 실행을 시켜야 한다.
우선 실행중인 스레드함수를 멈추게 할때는 우리가 리턴값으로 돌려받은 CwinThread*의 변수를 가지고 작업을 할 수 있다.
CwinThread::SuspendThread()
위의 함수가 바로 실행중인 스레드를 멈출 수 있게 하는 함수이다. 이 함수는 현재 실행중인 스레드 내부에서 호출할 수도 있고, 또한 다른 쓰레드 내에서도 호출할 수가 있다.
CwinThread::ResumeThread()
위 함수는 중지된 쓰레드를 깨우는 함수이다. 이 함수는 위의 SuspendThread같이 스레드 자기자신의 내부에서는 호출할 수가 없다. 다른 쓰레드나 메시지 Handler 함수들을 이용해서 ResumeThread()를 호출해야지 멈춰진 쓰레드를 다시 실행 할 수 있다.
(참고) 스레드 내부로 복잡하게 들어가면 내부적으로 각 스레드 함수는 Suspend count란 것을 유지하고 있어서 이 값이 0일 때만 스레드가 CPU시간을 할당받아 실행이 된다. 즉 AfxBeginThread함수에 dwCreateFlags에 CREATE_SUSPEND를 넣어줘서 실행한다거나 CwinThread::SuspendThread()를 실행하거나 하면 Suspend count는 1이 증가가 된다. 그래서 스레드는 멈추게 되고 다시 CPU로부터 시간을 할당받아 스레드가 활동을 재개하게 하려면 ResumeThred()를 호출해서 suspend count를 0으로 만들어 주어 CPU시간을 할당받게 해서 스레드를 다시 시작한다. 내부적으로 이런 메커니즘을 가지고 스레드는 동작하기 때문에 SuspendThread()를 두 번 호출하면 suspend count가 2가 되고, RecumeThread()도 두 번 호출해 줘야 스레드가 다시 활동할 수가 있다.
7. 스레드 잠재우기
쓰레드를 잠재우는 이유는 크게 두 가지가 있다.
하나는 에니메이션을 구현하거나 아날로그 시계의 바늘 같이 시간이 경과함에 따라 화면에 그림을 그린다든지 하는 경우, 즉 시간 경과에 따라 실행하여야 하는 프로그램의 경우이고,
두 번째는 어떤 스레드가 현재 실행되어야 하는 시점에서 다른 쓰레드에게 양보를 할 경우에 사용된다.
첫 번째의 경우는, 한마디로 쓰레드 내부에서만 사용하는 쓰레드 타이머라고 생각할 수 있다. 즉 일정시간이 지나면 반복적으로 작업을 할 때 유용하다.
쓰레드 내부에서도 Sleep()라는 걸 이용하면 쓰레드 내부 루틴을 타이머를 사용했을 때와 비슷하게 만들 수 있다.
쓰레드를 잠재울 때는 다음의 함수를 사용한다.
::Sleep( 0 );
위 함수는 win32 API함수이다. 따라서 사용할 때 :: 를 이용해서 호출한다. 만약 위의 함수를
::Sleep( 5000 );
하면 이 쓰레드는 5초 동안 CPU시간을 쓰지 않게 되며, 이렇게 잠들어 있을 때는 다른 쓰레드가 CPU시간을 얻어서 활동하게 된다.
두번째는 위의 첫 번째 예와 같이 인자로 0을 주는 경우인데, 이 경우에 현재 스레드를 정지하고(0 이면 0.001초인 이데..멈추다니..) 이 스레드가 CPU시간을 풀어줄 때까지 기다리던 다른 스레드가 실행된다. 그런데 다른 스레드는 이 스레드와 우선순위가 같아야 한다. 그리고 같은 우선순위의 스레드가 없다면 현재 sleep(0)을 실행했던 스레드가 계속 실행이 된다.
스레드 안에서 sleep()를 쓰는 경우는 시간의 지연을 두고 작업을 해야 하는 경우에 많이 사용된다. (ex. 아날로그 시계)
CwinThread* pThread = AfxBeginThread( TimeFunc, pParam );
UINT TimeFunc( void* pParam )
{
void* param = (void*) pParam; // 사용자가 넘겨준 데이터를 사용하기 위함
while( true )
{
::Sleep( 1000 ); // 이곳에서 1초가 멈춰진다.
DrawTime(); // 초침을 그리는 함수
}
}
7. 스레드 죽이기
쓰레드를 죽이는 이유는 자기의 일을 다 마치고 나면 당연히 쓰레드는 없어져야 하기 때문이다. 그렇지 않으면, 쓰레드는 여전히 자기가 들어앉은 메모리 속에 앉아서 컴퓨터 리소스만 잡아먹고 있게 된다.
스레드를 죽이는 방법엔 두 가지가 있다.
① 스레드 내부에서 return을 시킬 때.
② AfxEndThread를 호출할 때.
안전한 방법은 스레드 내부 자체에서 return문을 이용하는 것이 안전합니다. 탐색기도 이와 같은 방법을 사용합니다. 복사가 다 끝나면 자동적으로 return을 시키기 때문에 스레드가 알아서 죽는다. 이런 방법은 쓰레드가 오랜 시간을 작업하는경우에 사용한다. 즉 언젠가는 결과를 얻어서 끝 날수 있는 일을 할 때 사용하게 되고, 대개 무한루프를 돌면서 하는 일은 AfxEndThread를 사용해서 끝낸다.
위의 첫 번째 방법과 같이 return을 받았을때는 GetExitCode를 이용해서 검색할 수 있는 32bit의 종료 코드를 볼수 있다.
DWORD dwexitcode;
::GetExitCodeThread( pThread‐>m_hThread, &dwExitCode );
// pThread는 CwinThread* 객체의 변수이고..
// m_hThread 는 CwinThread내부의 생성된 쓰레드 핸들
이 문장은 실행이 종료된 스레드의 상태를 보여주는 문장이다. dwExitCode에 스레드가 return 하면서 나온 32bit의 종료값이 들어가게 된다. 만약 실행중인 스레드를 대상으로 저 코드를 쓰게 된다면 dwExitCode에는 STILL_ACTIVE라는 값이 들어가게 된다.
그런데, 위의 코드를 사용함에 있어 제약이 있다. CwinThread*객체는 스레드가 return 되어서 스스로 종료가 되면 CwinThread 객체 자신도 혼자 제거되어 버린다. 따라서 delete시켜주지 않아도 메모리에서 알아서 없어진다.
return이 되어서 이미 죽어버린 스레드를 가지고 pThread‐>m_hThread를 넣어주면, 이것은 이미 return되어 죽어버린 스레드의 핸들을 가리키고, Access위반이란 error메시지가 나오게 된다.
이런 문제를 해결하려면 CwinThread* 객체를 얻은 다음 이 객체의 멤버 변수인 m_hAutoDelete를 FALSE로 설정하면 스레드가 return을 해도 CwinThread객체는 자동으로 제거 되지 않기 때문에 위의 코드는 정상적을 수행된다.
이런 경우에 CwinThread*가 더 이상 필요가 없어지면 개발자 스스로 CwinThread를 delete시켜 주어야 합니다. 또 다른 방법으로 스레드가 가동이 되면 CwinThread*의 멤버변수인 m_hThread를 다른 곳으로 저장을 해놓고 이 것을 직접GetExitCode()에 전달을 하면 그 쓰레드가 실행중인지 한때는 실행되고 있었지만 죽어버린 스레드인지 확인이 가능하다.
int a = 100; // 파라미터로 넘겨줄 전역변수.
CwinThread* pThread // 전역 쓰레드 객체의 포인터 변수.
HANDLE threadhandle; // 스레드의 핸들을 저장할 핸들변수.
Initinstance() // 프로그램초기화.
{
// 프로그램 실행과 동시에 스레드 시작.
1번방법:pThread = AfxBeginThread( func, (int) a );
// 스레드가 리턴되면 자동으로 CwinThread객체가 자동으로 파괴되지 않게 설정.
2번방법:pThread‐>m_hAutoDelete = FALSE;
// 쓰레드 핸드를 저장. 위의 m_hAutoDelete를 설정하지않았을경우..
threadhandle = pThread‐>m_hThread;
}
MessageFunction() // 어떤 버튼을 눌러서 스레드의 상태를 알고 싶다..
{
char* temp;
DWORD dwExitcode;
// 스레드 객체의 m_hAutoDelete를 fasle로 설정해서 스레드가 return되어도
// 객체가 자동으로 파괴되지 않아서 핸들을 참조 할수 있다.
1번방법: ::GetExitCode( pThread‐>m_hThread, &dwExitcode);
// 스레드가 종료되고 미리 저장해둔 핸들을 이용할경우..
2번방법: ::GetExitCode(threadhandle, &dwExitcode);
sprintf( temp, “Error code : %d”, dwExitcode );
// 스레드 객체 삭제..
1번방법: delete pThread;
AfxMessageBox( temp );
}
func( void* pParam )
{
int b = (int) pParam;
for( int I = 0; I < b; I++)
{
// 어떤일을 한다.
}
return; // 작업이 끝나면 리턴한다. 이때 스레드 자동으로 종료.
}
위의 코드는 어떠한 작업을 수행하는 쓰레드를 돌리고 작업이 끝나면 자연적으로 리턴을 시키는 가상 코드이다.
1번째 방법은 스레드를 생성하고 m_hAutoDelete를 false로 해서 스레드가 return해서 자동종료해도 CwinThread를 자동파괴하지 않게 하고, GetExitCodeThread()를 호출한다. 밑에서 delete 는 꼭 해야한다.
2번째는 m_hThread를 다른 핸들변수에 저장해 놓고..스레드가 return되면 CwinThread*도 같이 파괴가 된다. 원래 저장한 핸들을 가지고 GetExitcodeThread()를 호출해서 한때 존재했지만 종료된 쓰레드를 검사하는 것이다.
7. 스레드 조작
◎ 하나의 스레드에서 다른 스레드 종료시키기..
① ================
두개의 스레드가 동작하고 있을 때 하나의 스레드에서 다른 스레드를 일방적으로 종료시키는 방법을 알아본다.
*** 스레드 A ***
static BOOL bContinue = TRUE;
CwinThread* pThread = AfxBeginThread( func(), &bContinue )
//.........
bContinue = FALSE;
*** 스레드 B ****
UINT func( LPVOID pParam )
{
BOOL* pContinue = (BOOL*)pParam; //스레드 A에서 넘겨준 종료플래그
While( *pContinue )
{
//..........
}
return 0; // 끝나면 스레드 B 자동종료.
}
일반적으로 스레드 사이에서 서로를 사살할 때 많이 쓰는 방법입니다. 이 방법은 보통 BOOL형식의 플래그를 하나 둠으로써 그 값을 어떤 스레드에서 변형하여 다른 스레드에서 그 값의 변화를 인지하게 함으로써 스레드를 자동종료( return )시켜버리는 것이다. 보통 이것은 스레드들이 통신하기에 적합하지 않은 방식이라고 많은 서적에서 설명하고 있다. 보통은 스레드들의 동기화 객체를 이용하는 적이 효과적인 방법이다.
우선은 스레드 A에서 static 형의 정적인 BOOL형 변수를 하나 만든다. Static으로 선언된 변수는 프로그램이 생성됨과 동시에 자동으로 메모리에 올라가 있게 된다. 그래서 그 변수가 선언된 블록을 빠져 나오게 되더라도 계속적으로 그 값을 유지하고 있다. 그래서 static이라고 플래그 변수를 선언하고 TRUE로 초기화 시켜 둔다.
그러면 스레드 B는 BOOL형 값의 플래그를 판단하여 실행여부를 결정하는 while루프안에다가 스레드에서 수행해야 할 일을 넣어주고, 스레드 A에서 더 이상 B가 존재할 이유가 없다고 생각하면 플래그의 값을 FALSE 로 변경시켜서 스레드 B를 자연스럽게 죽게한다.
그런데 이 방법은 대부분 사용되지 않는다.(비효율적이므로) 왜냐하면, 스레드 A에서 bContinue = FASLE로 설정한 다음 어떤 작업을 다시 들어가는데, 만약 CPU에서 스레드 스케줄링을 잘못하여 스레드 B가 종료되기 전에 스레드 A에서 또 다른 어떤 작업을 들어가야 한다면, 원하지 않았던 결과를 초래한다.
② ====================
그래서 우리는 이때 스레드 A에서 스레드 B가 죽을 때까지 기다리는 매커니즘(방법)이 필요하다.
// 스레드 A
static BOOL bContinue = TRUE;
CwinThrad* pThread = AfxWinThread( func(), &bContinue );
// 어떤일 수행.
// 생성된 스레드의 핸들을 HANDLE변수에 저장.
HANDLE hthread = pThread‐>m_hThread; //m_hThread는 생성된 스레드의 핸들
BContinue = FALSE;
::WaitForSingleObject( hthread, INFINITE ); //졸라리 중요한 함수임다
// 스레드 B
UINT func( LPVOID pParam )
{
BOOL* pContinue = (BOOL*) pParam;
While( *pContinue )
{
// 어떤일 수행.
}
return 0;
}
WaitForSingleObject란 Win32함수의 프로토타입은 다음과 같다.
DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilisecond );
첫번째 인자로 주어진 핸들에 위의 예문에서 실행시킨 스레드의 핸들값을 저장한다. 그리고 두번째 인자로 시간값을 받는데, 이 값은 어떤 행동이 일어나기까지 기다릴 시간을 말한다. 이 함수의 목적은 첫번째 핸들값으로 주어진 그 핸들에 대한 해당하는 어떤객체(?)가 “신호를 받은 상태” 가 될 때까지 기다린다. 예제를 보면 스레드A가 스레드B를 종료시키려고 종료플래그를 FALSE로 셋팅하고 waitforsingleobject()를 호출하고 있는데, 이때 신호를 받아야하는 객체로 스레드B의 핸들을 지정하는 것이다. 이 말은 스레드 B가 정상적이든, 혹은 종료신호가 올 때까지 기다린다는 의미이다.
네트웍 프로그램에서 어떤 파일을 받는 스레드가 있고, 이 받은 데이터로 먼가를 작업하는 스레드 2개가 존재한다면 모든 데이터가 다 받아 질 때까지 받은 데이터로 작업하는 스레드는 멈춰 있어야한다. 이 경우 저 함수를 이용해서 받은 데이터로 작업을 수행하는 스레드를 멈춰 둠으로써 안정적으로 작동하게 하는 것이다.
이때 WaitForSingleObject에 두번째 인자에 INFINITE를 넣지 않고 5000이라는 값을 넣으면 무한정 기다리는 대신 그 스레드가 신호를 받기를 기다리는 시간을 5초를 지정한 겁니다. 여기서 5초가 경과된 후에도 이 객체가 신호를 받지 못하면, WaitForSingleObject는 반환이 되는데 이 반환된 값을 가지고도 프로그래머가 스레드의 상황을 파악할 수 있다.
WAIT_OBJECT_0 : 객체가 신호를 받았음
WAIT_TIMEOUT : 객체가 신호를 받지 못했음
//스레드 A(받은 데이터를 가지고 어떤 작업을 한다..)
………
if(::WaitForSingleObjcet(hThread, 5000) == WAIT_TIMEOUT )
{
AfxMesageBox(“5초가 지나도록 데이터 못 받았다..에러다..”)
}
else
{
//데이터 다 받았으니까..알아서..해라..
}
//스레드 B(데이터를 받는다..)
…작업중…
다른 방법으로는 WaitForSingleObjct의 두번째 인자에 0을 지정함으로써 이 스레드 함수가 실행중인지 혹은 종료가 되었는지도 알 수 있다.
if( ::WaitForSingleObjcet( hThread, 0 ) == WAIT_OBJCET_0 )
{
// 스레드가 종료가 됬당..
}
else
// 스레드가 아직 실행중이다..
즉 두 번째 인자에 0이란 값을 주면, 그 스레드에 대한 상태를 바로 반환해 주게 된다.
CwinThread*의 멤버변수중에 하나인 m_bAutoDelete를 false로 설정해 놓지도 않고, CwinThread가 신호를 받았는지 알기위해서 WaitforSingObject를 호출하는 것은 말이 안됩니다. 이유는 그 스레드가 끝이나면 해당 CwinThread*객체 또한 자동으로 사라지기 때문에, m_bAutoDelete를 false로 해주지 않으면, 말이 안된다.
③ =====================
마지막 방법은 ::TerminateThread( pThread‐>m_hThread , 0 ); 이다.
이것은 어떤 스레드에서 다른 스레드를 종료시킬 때 최후의 수단으로 쓸 때 요긴하다. 위의 함수를 사용하면 pThread‐>m_hThread를 종료하고, 그 스레드에 종료코드 0을 할당한다. 이 함수는 사용함에 있어 제약이 많이 따른다.
예를 들어 동기화 객체를 예로 들수 있는데, 어떤 동기화 객체를 공유하는 스레드들이 있는데.. 어떤 스레드가 공유하는 동기화 객체를 Locking하고 작업을 수행하던중,(이때 다른 스레드들은 공유 동기화 객체를 얻기 위해 대기상태에 있다) 위의 TerminateThread함수를 사용해서 작업중인 스레드를 죽이면, 공유된 동기화 객체를 UnLocking하기 전에 죽어 버릴 수 있게 된다. 이러면 공유된 동기화 객체는 영원히 Locking되어 있기 때문에 그 동기화 객체를 공유하는 다른 스레드들은 실행이 안 되게 된다.
=======================
=======================
=======================
1. C/C++프로그래밍과 ::CreateThread
윈도우가 제공하는 CreateThread 함수는 스레드를 생성하는 함수이다. 하지만 C/C++ 로
코드를 작성하는 경우에는 CreateThread 를 사용해서는 안 되고, 마이크로소프트 C/C++
runtime-library 에서 제공하는 _beginthreadex 함수를 사용해야 한다. 다른 컴파일러
에서도 ::CreateThread 함수를 대체할 만한 함수를 제공할 것이며, 반드시 컴파일러에 의해
제공되는 다른 함수를 사용해야 한다.
2. 멀티 스레드 안전한 C/C++ Library
역사적으로 C runtime-library 개발자는 멀티 스레드 어플리케이션에서 C runtime-library 를
사용하였을 때 발생하는 문제에 대해서는 전혀 고려하지 않았다. 멀티 스레드 어플리케이션
에서 전통적인 C runtime-library 를 사용하였을 때 문제가 발생할 수 있다.
따라서 Microsoft 는 이러한 문제를 해결하기 위해서 스레드 안전한 C/C++ runtime-library 를
제공하고 있다.
멀티 스레드 안전한 C/C++ run-time library 함수는 다른 스레들로부터 영향을 받지 않도록
자신을 호출한 스레드의 데이터 블록에만 접근 가능하게 한다.
3. Single-thread C/C++Library 와 Multi-thread C/C++ Library
Single-thread C/C++Library 는 단일 스레드 전용의 함수들을 말하고,
Multi-thread C/C++ Library 는 멀티 스레드 전용의 함수들을 말한다.
※ Visual Studio 2008 에서는 Multi-thread C/C++ Library 만 지원한다.
즉 더 이상 단일 스레드 전용의 C/C++ 라이브러리는 제공하지 않는다.
4. _beginthread 와 _endthread
_beginthread 함수는 새로운 스레드를 생성하고 난 후 바로 ::CloseHandle 함수를 호출하여
새로 생성된 스레드의 핸들을 제거하게 된다. 따라서 _beginthread 함수 호출 이후에 이 스레드
핸들에 접근 할 수 없게 된다.
_beginthread 함수의 이런 동작은 Win32의 상세함을 숨기기 위해 고안되었으나 결국 버그가
되어버린 함수이다. 따라서 마이크로소프트는 이러한 버그를 수정한 _beginthreadex 함수를 만들
게 되었다.
_beginthread 함수는 ::CreateThread, _begintheadex 함수에 비해 매개변수의 개수가 적다.
보안특성을 가진 스레드를 생성할 수 없으며, 일시 정지된 상태의 스레드도 생성할 수 없고,
스레드의 ID 값을 얻을 수도 없다.
_beginthread 함수는 스레드가 종료되면 자동으로 _endthread 함수를 호출 하는데 이 함수는
어떠한 매개변수도 가지고 있지 않기 때문에 스레드의 종료 코드는 항상 0 이다.
5. _beginthreadex 와 _endthreadex
::CreateThread 함수의 문제점을 보완하기 위해 C/C++ runtime-library 가 제공하는 스레드를
생성하는 함수로서 각 스레드 별로 적절한 구조의 데이터 블록을 생성해 준다.
::CreateThread 함수와 동일한 매개변수를 가지고 있지만 매개변수의 이름과 형태가 일치 하지
않는다. C/C++ run-time library 가 윈도우의 자료형에 의존성을 가지지 않도록 개발했기 때문
이다.
_beginthreadex 함수의 반환값은 ::CreateThread 함수의 반환값과 같은 새로 생성된 스레드의
핸들 값이다. 하지만 자료형이 정확하게 일치하지는 않기 때문에 적절하게 형변환을 해줘야 한다.
_beginthreadex 함수는 _beginthread 함수와는 달리 내부적으로 새로 생성한 스레드의 핸들을
제거하지 않기 때문에 명시적으로 ::CloseHandle 함수를 호출해 주어야 한다.
_beginthreadex 함수에 의해 생성된 스레드가 종료되면 _endthreadex 함수가 자동으로 호출.
6. ::AfxBeginThread
MFC 에서 스레드(Worker thread, User interface thread)를 생성하는 함수이다.
::AfxBeginThread 함수는 실제 스레드 생성을 수행하지 않는다. 스레드 생성을 위한 객체를
생성하고 스레드 생성을 위해 CWinThread::CreateThread 함수를 호출하는 것이 전부이다.
실제 스레드 생성은 CWinThread::CreateThread 함수 내부에 구현되어 있다.
CWinThread::CreateThread 함수는 내부적으로 _beginthreadex 함수를 호출하여 스레드를
생성한다.
먼저, _beginthread(ex)() 함수나 AfxBeginThread() 함수는 같은 계열의 함수입니다.
앞의 함수를 내부적으로 wrapping 한 경우가 MFC의 AfxBeginThread()함수입니다.
결정적으로 _beginthread()는 쓰레드를 생성하고 돌리는 과정에서 자체의 메모리 공간을 설정하고 보장을
합니다. 따라서 내부에 static variable이나 static buffer를 사용하는 과정에서 오류가 발생하지 않습니
다.
그러나 CreateThread()계열의 함수들은, 독자적인 메모리 공간을 설정하지 않으므로, 사용하면 memory leak
이 발생합니다. 따라서 사용하지 않는게 좋은 방법이지요.(가급적)
그리고 MSDN의 마지막 부분은 저두 읽을때, 약간 애매했던 부분인데, 머라 딱 꼬집어서 말씀드리기가 그렇
네요...
http://bwangel.egloos.com/923635
=======================
=======================
=======================
출처: http://kuaaan.tistory.com/50
CreateThread는 생성된 쓰레드 내에서 CRL 계열의 함수를 사용할 때 TLS를 사용하지 않기 때문에 Thread-Safe하지 않은 문제가 있다는 것은 널리 알려져 있다. 대신 _beginthreadex 나 AfxBeginThread를 사용하라고들 얘길 하는데....
AfxBeginThread 함수의 문제는 아니지만... AfxBeginThread를 다른 쓰레드 생성 함수들처럼 사용해버리면 문제가 되곤 한다.
먼저 _beginthreadex로 쓰레드를 생성하는 샘플코드이다.
view plaincopy to clipboardprint?
- HANDLE hThread;
- unsigned int threadID;
- hThread = (HANDLE)_beginthreadex( NULL, 0, &SecondThreadFunc,
- NULL, 0, &threadID );
- if ( hThread != NULL)
- CloseHandle( hThread );
스레드 생성시에는 스레드 내부적으로 별도의 핸들을 가지고 있기 때문에 _beginthreadex가 리턴하는 핸들까지 하면 두개의 핸들이 생성된다. 따라서, 생성한 스레드를 부모 스레드에서 계속 관리할 것이 아니라면 바로 CloseHandle 해버리는 것이 자원을 관리하는 FM이다. (그러지 않는다면 나중에 스레드가 종료된 후에도 스레드 Object가 파괴되지 않을 것이다. 왜냐구? 핸들카운트가 0이 되지 않았기 때문이지!)
그런데... MFC계열의 AfxBeginThread는 사용법이 약간 다르다.
MSDN의 함수 Prototype은 다음과 같다.
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
일단... 리턴값이 다르다. 스레드의 HANDLE이 아니라 CWinThread 클래스의 포인터이다.
CWinThread? 이건 또 모야... MFC코드에는 다음과 같이 선언되어 있다.
class CWinThread : public CCmdTarget
{
DECLARE_DYNAMIC(CWinThread)
public:
// Constructors
CWinThread();
// ... 중간생략
// only valid while running
HANDLE m_hThread; // this thread's HANDLE
operator HANDLE() const;
DWORD m_nThreadID; // this thread's ID
아항... CWinThread의 내부에 스레드 핸들을 가지고 있군.
흠. 그렇다면 이렇게 코드를 써주면 되겠구만
view plaincopy to clipboardprint?
- CWinThread* pWinThread = NULL;
- pWinThread = AfxBeginThread(pfnThreadEntryFunc, this);
- if (pWinThread == NULL)
- return FALSE;
- CloseHandle (pWinThread->m_hThread);
바로 이게 오바질이 되것다.
저렇게 하면... 스레드 종료할 때(정확히 말하면 CWinThread 개체 파괴시) 원인불명의 메모리 폴트가 뜨게 된다. 왜냐구?
AfxBeginThread에 의해 생성된 스레드가 종료될 때 CWinThread의 소멸자가 호출되는데, 이 소멸자 내에서 CWinThread 개체의 m_hHandle을 CloseHandle하도록 되어 있다. 이때 이미 Close된 핸들을 다시 Close하려고 시도하니까 문제가 발생하는 것이다.
AfxBeginThread로 스레드를 생성했을 경우에는 굳이 CloseHandle을 해줄 필요가 없다.
꼭 해주려면 다음과 같이 해주면 에러가 발생하지 않는다.
view plaincopy to clipboardprint?
- CWinThread* pWinThread = NULL;
- pWinThread = AfxBeginThread(pfnThreadEntryFunc, this);
- if (pWinThread == NULL)
- return FALSE;
- CloseHandle (pWinThread->m_hThread);
- pWinThread->m_hThread = NULL;
※ 만약 부모 스레드에서 자식스레드의 상태를 계속 관리해주어야 하는 경우에는
다음과 같이 스레드 종료 후 CWinThread가 자동 파괴되지 않도록 해주어야 한다.
view plaincopy to clipboardprint?
- CWinThread* pWinThread = NULL;
- pWinThread = AfxBeginThread(pfnThreadEntryFunc, this);
- if (pWinThread == NULL)
- return FALSE;
- pWinThread->m_bAutoDelete = FALSE; // 자동파괴되지 않도록 설정
- // 여기서 WaitFor.. 나 GetExitCodeThread 같은 관리 코드를...
- // ...
출처: http://kuaaan.tistory.com/50 [달토끼 대박나라~!! ^^]
=======================
=======================
=======================
퍼온곳 : http://www.zdnet.co.kr/builder/dev/c/0,39030803,10062429,00.htm
글쓴이 : 이현창 (아주대학교) 2003/06/29
프로젝트에 다중 쓰레드를 도입하면 동기화나 종료 처리 등의 문제로 고려해야 할 사항들이 곱절 이상이나 늘어나게 된다. 그럼에도 불구하고 다중 쓰레드를 사용하는 이유는 프로그램의 성능을 향상시켜 주기 때문이다. 여기서 성능을 향상시켜 준다는 말이 무엇을 의미하는지는 좀더 생각해 볼 필요가 있다. 또한 어떻게 해야 성능이 향상되는지도 알아 볼 필요가 있다.
다중 쓰레드에 대해 공부해 본 적이 있다면 많은 책과 기사에서 다중 쓰레드의 사용을 될 수 있으면 자제하라고 권장한다는 사실을 알고 있을 것이다. 일단 프로젝트에 다중 쓰레드를 도입하면 동기화나 종료 처리 등의 문제로 고려해야 할 사항들이 곱절 이상이나 늘어나게 된다.
상식적으로 생각해도 알 수 있듯이 코드 한 줄을 고치는 데도 많은 고민을 해야 하며, 문제 상황이 항상 재현되는 것이 아니기 때문에 디버깅 시에도 많은 고생을 하게 된다. 그럼에도 불구하고 다중 쓰레드를 사용하는 이유는 프로그램의 성능을 향상시켜 주기 때문이다. 그런데 여기서 성능을 향상시켜 준다는 말이 무엇을 의미하는지는 좀더 생각해 볼 필요가 있다.
또한 어떻게 해 성능이 향상되는지도 알아 볼 필요가 있다. 실제로 다중 쓰레드를 사용한 프로젝트 중에는 성능 향상은 별로 보이지 않고, 다중 쓰레드 도입으로 인한 문제점만 고스란히 떠안고 있는 경우가 많기 때문이다.
다중 쓰레드의 도입 시기
컴퓨터에 설치된 MSDN이나 웹에서 ‘Win32 Multithreading Performance’라는 제목의 기사를 찾아보자. 이 기사는 1996년도 1월에 작성된 기사이지만 언제 다중 쓰레드를 적용하는 것이 좋은지 아주 훌륭하게 설명하고 있다. 그렇기 때문에 필자는 새로운 설명 방식과 예제를 구상하는 시간에 차라리 이 기사의 주요 내용에 자세한 설명과 그림을 추가해 여러분의 이해를 돕는 것이 더 효율적일 것이라는 결정을 내렸다. 이 단락을 읽고 나면 여러분은 어떤 상황에서 다중 쓰레드를 사용해야 하는지 판단할 수 있는 능력을 지니게 될 것이다. 크게 세 가지의 주요 관점을 기준으로 설명을 하려고 한다.
전체 작업의 완료 시간
CPU가 하나만 존재하는 시스템에서 실제로 두 작업이 동시에 실행되는 것은 불가능하다. CPU가 아주 짧은 시간 간격으로 두 작업을 번갈아가며 실행시키기 때문에 우리에게 마치 동시에 실행되는 것처럼 느껴지는 것뿐이다. 이 때 우리가 간과해서는 안될 사실은 두 작업 사이를 오가는 과정(context switching)에서 오버헤드가 발생한다는 점과 쓰레드를 생성하는 일 역시 시간을 소모하는 작업이라는 점이다. 예를 들어 두 개의 작업 A, B가 있다고 하자. 이 경우 다중 쓰레드를 사용한 경우와 그렇지 않은 경우의 전체 작업의 완료시간은 다음과 같다. 다중 쓰레드를 사용한 경우가 보다 비효율적이라는 것을 알 수 있다.
◆ 다중 쓰레드를 사용한 경우의 전체 작업 완료 시간
= A의 작업 시간 + B의 작업 시간 + Context Switching 소요 시간 + 쓰레드 생성 소요 시간
◆ 다중 쓰레드를 사용하지 않은 경우의 전체 작업 완료 시간
= A의 작업 시간 + B의 작업 시간
하지만 이는 A, B가 작업 시간 내내 순수하게 CPU만 사용한다는 가정에서만 성립된다. A, B의 작업 시간 중에 I/O 요청을 기다리는 시간이 포함된다면 문제가 달라진다. 예들 들어 A의 주요 작업 내용이 메모리의 데이터를 특정 파일에 기록하는 것이라고 가정하자. 파일에 기록하는 작업은 하드디스크와 같은 하드웨어에 기록을 요청한 후에 하드웨어에서 완료 응답이 도착할 때까지 기다리는 방식으로 진행된다. 그런데 이렇게 하드웨어의 응답을 기다리는 동안에는 CPU를 사용하지 않기 때문에 다른 작업이 CPU를 사용할 수 있다. 다시 말해 A가 I/O 요청을 기다리는 동안에는 B가 자신의 작업을 수행할 수 있다는 말이다. 그래서 다중 쓰레드를 사용한 경우 전체 작업의 완료 시간은 다음과 같이 다시 계산될 수 있다.
다중 쓰레드를 사용한 경우의 전체 작업 완료 시간
= A의 작업 시간 - A의 I/O 요청 대기 시간 + B의 작업 시간 + Context Switching 소요 시간 + 쓰레드 생성 소요 시간
결론적으로 I/O 작업은 다중 쓰레드의 좋은 대상이 된다고 할 수 있다. 운영체제의 발전 역사를 보더라도 하나의 시스템에 여러 사용자 프로그램을 동시에 적재해 실행하기 시작한 계기도 한 프로세스가 I/O 요청을 기다리는 동안에 다른 프로세스가 CPU를 사용할 수 있다는 사실을 운영체제 설계자들이 깨달았기 때문이다.
처리 시간 = 대기 시간 + 작업 시간평균 처리 시간
앞서 CPU 연산의 경우 다중 쓰레드는 전체 작업 완료 시간을 단축하는 데 도움이 되지 않는다는 점을 확인했다. 그러나 우리의 관심사를 평균 처리 시간으로 옮기게 되면 CPU 연산의 경우에도 다중 쓰레드를 사용해 성능을 향상시킬 수 있다는 점을 확인할 수 있다. <그림 1>을 보면서 설명하도록 하겠다.
작업 A, B, C는 각각 1, 4, 3의 작업 시간을 갖으며 CPU 연산이다. 이 때 A, B, C의 순서대로 작업을 실행시킨다고 해보자. 그러면 작업 A는 바로 시작해서 작업 시간인 1만큼의 시간이 경과한 후에 완료될 것이다. 작업 B의 경우는 A가 끝나는 시간인 1만큼 기다렸다가 작업 시간인 4만큼의 시간이 경과한 후에 완료될 것이다. 전체적으로 볼 때 작업 B는 5만큼의 시간이 지난 후에 완료되는데 이 시간을 처리 시간(Turnaround Time)이라고 부르도록 하자.
같은 식으로 C의 처리 시간은 8이 된다. 이 글에서는 이러한 처리 시간의 평균을 ‘평균 처리 시간’이라고 부르도록 하겠다. 이제부터 우리는 다중 쓰레드를 사용하는 것이 어떻게 평균 처리 시간을 줄일 수 있는지 확인시켜 줄 예제 하나를 보게 될 것이다.
<그림 2> 평균 처리 시간의 감소 예
이런 종류의 문헌에서 자주 등장하는 슈퍼마켓에서의 계산 예를 들어보자. 슈퍼마켓에서 여러 손님들이 자신이 필요한 물건을 고른 후에 계산을 하려고 한다. 그리고 점원은 딱 한 명밖에 없다. 한 명의 점원이 여러 손님의 물품을 계산하는 방법으로 크게 두 가지를 생각해 볼 수 있다. 하나는 사람들을 한 줄로 세우고 차례로 계산을 해주고, 다른 하나는 여러 개의 계산대에 사람들을 여러 줄로 세우고 한 번에 한 계산대씩 돌아가면서 차례로 계산을 해주는 방법이다. 그림을 좀더 설명하면 이렇다.
각 손님마다 계산해야 할 물품의 개수가 적혀져 있는데 점원은 한 번에 하나씩 물품 계산을 한다고 가정한다. 즉, 10개의 물품을 가진 손님은 5개의 물품을 가진 손님보다 계산 시간이 두 배가 길어지게 된다. 마찬가지로 두 번째 방법의 경우에도 점원이 한 번에 한 물품만 계산하게 된다. 즉, 손님이 가지고 온 물품 중에 하나만 계산하고 다른 계산대로 이동하는 방식이다. 두 방법은 각각 다중 쓰레드를 사용한 경우와 사용하지 않은 경우를 비유하기 위한 것이다. 각 손님은 작업에 해당하며, 손님이 가지고 있는 물품은 작업 시간에 해당한다. 계산대 혹은 손님들의 줄은 하나의 쓰레드에 해당하고, 점원은 CPU에 해당한다. 따라서 두 번째 방법은 다중 쓰레드를 비유하고 있다는 점을 쉽게 알 수 있을 것이다.
그림을 보면 물품을 한 개만 가지고 있는 손님이 보일 것이다. 여러분이 그 손님이라고 상상해 보자. 첫 번째 그림에서 계산을 끝내고 슈퍼마켓을 나오기 위해서는 10 + 5 + 2 + 8 + 1 = 26만큼의 시간이 걸릴 것이다. 반면에 두 번째 그림에서는 5만큼의 시간 후에 슈퍼마켓을 나올 수 있다. 즉, 전체 손님을 놓고 볼 때는 이득이 없지만 여러분 입장에서는 많은 시간을 벌 수 있는 것이다. 그러면 두 그림의 경우에서 평균 처리 시간은 어떻게 되는지 계산해 보자. 첫 번째 그림은 직관적이지만 두 번째 그림의 경우는 연습장에 그려가면서 계산해야 이해가 쉽다.
첫 번째 그림에서의 평균 처리 시간 = 10 + (10 + 5) + (10 + 5 + 2) + (10 + 5 + 2 + 8) + (10 + 5 + 2 + 8 + 1) = 93 / 5
두 번째 그림에서의 평균 처리 시간 = 26 + 12 + 8 +24 + 5 = 75 / 5
예상했던 것처럼 두 번째 그림의 경우에 평균 처리 시간이 적은 것을 볼 수 있다. 이제는 다중 쓰레드를 사용하는 것이 어떻게 평균 처리 시간을 줄일 수 있는지 이해할 수 있을 것이다.
그런데 앞의 예처럼 작업 시간을 미리 알 수 있는 경우라면 다중 쓰레드를 사용하지 않더라도 평균 처리 시간을 최소로 만들 수 있다. 작업 시간이 작은 작업을 먼저 수행하면 최소의 평균 처리 시간을 얻을 수 있는 것이다. 다시 말해 작업을 1, 2, 5, 8, 10의 순서로 진행하면 평균 처리 시간이 1 + (1+2) + (1+2+5) + (1+2+5+8) + (1+2+5+8+10) = 54/5로 가장 작은 값을 갖는다. 이런 방식으로 정렬하는 경우에 최소의 평균 처리 시간을 얻을 수 있다는 사실은 직관적으로도 알 수 있으며 쉽게 증명할 수 있다.
또 한 가지 간과하기 쉬운 사실이 있다. <그림 2>의 두 번째 그림에서 5명의 손님이 같은 셔틀 버스를 타고 집에 가야 하는 경우를 생각해 보자. 아무리 일찍 계산을 마치고 나왔다 하더라도 결국은 마지막 손님이 계산을 마치고 셔틀버스에 탈 때까지 기다려야 한다. 바꿔 말해 다중 쓰레드를 사용해 처리된 작업들의 결과가 결국은 한 데 모여야지만 된다면 평균 처리 시간을 줄이는 것은 아무런 의미가 없게 되는 것이다.
반응성
지금까지 살펴본 것 외에 다중 쓰레드를 적용해 달성할 수 있는 요소 중에 한 가지는 반응성이다. 이는 실제적인 예를 들어 설명하는 것이 더 이해하기 쉬울 것 같다. 압축 프로그램을 만든다고 생각해 보자. 만약에 다중 쓰레드를 적용하지 않고 압축 해제 루틴을 구현하게 된다면 사용자는 압축 해제가 끝날 때까지는 작업을 취소할 수 없을 것이다. 사용자가 예상 시간이 30분이나 되는 압축 파일을 풀다가 취소를 하려고 했다면 사용자는 프로그램을 강제로 종료시켜 버리고는 다시는 쓰지 않을 것이다. 반면에 별도의 쓰레드에서 압축 해제 작업을 하는 경우에는 사용자의 입력을 받을 수 있기 때문에 취소 기능을 구현하는 것이 가능하다. 즉, 사용자의 요청에 빠르게 반응하는 프로그램을 만들 수가 있다.
요약 : I/O 연산은 다중 쓰레드를 적용하기에 아주 좋은 후보가 된다(경우에 따라 단일 쓰레드를 사용한 비동기 I/O가 다중 쓰레드를 사용한 동기 I/O보다 나을 수도 있다). CPU 연산의 경우에 평균 처리 시간을 줄이는 것이 중요하지 않다면 하나의 작업 쓰레드에서 모든 작업을 하는 것이 좋다. 마지막으로, 처음에 언급했던 것처럼 같은 효과라면 다중 쓰레드의 사용을 자제하는 것이 좋다.
volatile 키워드의 사용
안타깝게도 C++를 사용해 쓰레드를 생성하고 제어하기 위한 코드는 플랫폼마다 다른 형식을 취하고 있다. 이는 쓰레드를 사용하는 프로젝트의 이식성을 떨어뜨리는 주요 원인 중 하나이다. 그러나 지난 번 기사에서 소개한 boost 라이브러리에는 플랫폼에 독립적인 쓰레드 클래스를 가지고 있으니 한번 확인해 보는 것도 좋을 것이다.
C++에는 volatile 키워드가 있는데, 사전 그대로 말하자면 ‘휘발성의’, ‘변덕이 심한’이라는 뜻이다. 이 키워드는 C++를 사용해 다중 쓰레드를 구현하는데 있어서 아주 중요한 역할을 한다. <리스트 1>은 하나의 작업 쓰레드를 가지고 있는 간단한 프로그램인데, 이를 릴리즈 버전으로 컴파일한 경우 올바르게 동작하지 않는다. 설명을 보기 전에 스스로 문제점을 찾아보도록 하자.
<리스트 1> volatile 키워드를 사용하지 않은 경우에 문제가 발생하는 코드 |
#include #include using namespace std bool finish = false; DWORD WINAPI WorkerThread(LPVOID); void main() { // 작업 쓰레드 시작 DWORD dwID HANDLE h = CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwID); Sleep(1000); // 작업 쓰레드가 종료하도록 유도 finish = true // 쓰레드 종료를 기다림 WaitForSingleObject(h, INFINITE); CloseHandle(h); } DWORD WINAPI WorkerThread(LPVOID) { cout << "Worker Thread started" << endl int i = 0 while(!finish) { ++i } cout << "Worker Thread finished : " << i << endl return 0 } |
힌트는 디버그 버전에서는 문제없이 돌아가고 릴리즈 버전에서만 문제가 발생한다는 점이다. 릴리즈 버전에서는 기본적으로 최적화를 수행하게 된다. 안타깝게도 최적화를 수행하는 컴파일러는 우리의 의도를 완벽하게 이해할 수 없다. WorkerThread 함수 안에서 finish 변수의 값을 true로 설정하는 곳이 없기 때문에 while(!finish)을 while(true)처럼 바꿔도 아무런 상관이 없을 것이라고 생각하는 것이다. 물론 매번 finish 변수에 접근하지 않아도 되기 때문에 속도 향상은 있겠지만 <리스트 1>의 경우엔 잘못된 판단이 아닐 수 없다.
결론적으로 main() 함수에서 finish 변수에 true 값을 넣어도 작업 쓰레드는 종료되지 않고 메인 쓰레드 역시 작업 쓰레드를 계속 기다리기 때문에 프로그램이 종료될 수 없다. 이 문제를 해결하는 방법은 매우 간단하다. volatile bool finish = false;과 같이 volatile 키워드를 넣어서 finish 변수를 선언하면 컴파일러는 함부로 finish 변수와 관련된 코드를 최적화하지 않는다.
HANDLE과 쓰레드 핸들
CreateThread() 함수가 성공적으로 호출되면 쓰레드의 핸들을 반환하게 된다. 아마도 여러분은 HANDLE 타입이 쓰레드 핸들을 담는 용도 외에 여러 곳에서 사용되는 것을 보았을 것이다. 파일 핸들이나 프로세스 핸들, 뮤텍스나 이벤트 같은 동기화 객체의 핸들 역시 HANDLE 타입으로 다뤄진다. 이렇게 HANDLE 타입으로 다뤄지는 객체들의 공통점은 커널 객체라는 점이다. 커널 객체라는 공통점을 가지고 있기 때문에 몇 가지 특징을 공유하고 있다.
기본적으로 이러한 핸들은 레퍼런스 카운팅 방식으로 동작한다. 이는 COM 객체가 레퍼런스 카운트를 유지하다가 마지막 클라이언트가 릴리즈했을 때 레퍼런스 카운트가 0이 되어 소멸되는 것과 같은 방식이다. 이벤트 객체를 처음 생성한 경우에는 레퍼런스 카운트가 1이 된다. 그리고 나서 CloseHandle()을 사용해 핸들을 닫아주면 레퍼런스 카운트가 하나 줄어 들어 0이 되고, 실제로 객체가 소멸된다. 커널 객체는 프로세스가 아닌 커널의 소유이다.
그렇기 때문에 커널 객체를 생성한 프로세스가 종료되더라도 다른 프로세스에서 해당 커널 객체를 사용중이라면 커널 객체는 살아있는 채로 유지된다. 한 가지 알아둘 점은 윈도우 98 계열의 운영체제에서는 이미 소멸된 커널 객체에 대해 CloseHandle()을 호출해도 아무런 반응이 없는 반면에 윈도우 NT 계열의 운영체제에서는 예외가 발생한다는 점이다. 만약에 다중 쓰레드를 사용하는 프로그램이 윈도우 98에서 잘 동작하다가 윈도우 2000에서 예외를 발생시킨다면 쓰레드의 종료 처리를 의심해 볼 필요가 있다.
쓰레드 핸들의 경우에는 초기의 레퍼런스 카운트가 2가 되는 특징을 가지고 있다. 하나는 CreateThread를 호출한 클라이언트에게 반환되는 핸들을 위한 레퍼런스이고, 다른 하나는 쓰레드 스스로를 위한 레퍼런스이다. 후자의 레퍼런스는 쓰레드가 종료한 경우에 감소한다. 이러한 사실을 통해 다음과 같은 점을 생각해 볼 수 있다.
쓰레드 핸들의 특징
쓰레드를 생성하자마자 CloseHandle()을 호출하는 것이 쓰레드를 종료하게 만들지는 못한다. 단순히 클라이언트에 주어진 레퍼런스만 하나 감소하고 쓰레드는 여전히 동작하게 된다. 나중에라도 이 클라이언트가 쓰레드의 상태를 검사할 일이 없다면, 굳이 HANDLE을 보관할 필요가 없기 때문에 오히려 쓰레드를 생성하자마자 CloseHandle()을 호출하는 것이 바람직할 수도 있다.
마찬가지로 쓰레드가 종료된 이후에도 클라이언트가 아직 쓰레드 핸들을 닫지 않았다면 핸들의 레퍼런스 카운트는 1이 되어 아직 살아있게 된다. 이 경우에 쓰레드는 종료되었지만 쓰레드 핸들을 가지고 작업을 하는 것은 가능하다. 예를 들어 GetExitCodeThread()를 사용해 쓰레드의 반환 값을 확인할 수 있다. 물론 쓰레드 핸들을 닫은 후에는 이러한 작업이 불가능하다.
HANDLE로 다뤄지는 커널 객체들의 또 한 가지 공통점은 신호를 받은 상태 혹은 신호를 받지 않은 상태를 가지고 있다는 점이다. 예를 들어 이벤트의 경우에는 SetEvent()를 했을 때 신호를 받은 상태가 되고 ResetEvent()를 했을 때 신호를 받지 않은 상태가 된다. 프로세스나 쓰레드 핸들의 경우에는 처음 생성시에는 신호를 받지 않은 상태이고 종료된 경우에 신호를 받은 상태가 된다. 이러한 상태를 활용하는 대표적인 API가 바로 WaitForSingleObject(), WaitForMultipleObjects(), MsgWaitForMultipleObjects()이다.
이러한 API는 HANDLE을 입력 인자로 가지고 있는데, 입력된 핸들이 신호를 받을 때까지 기다리는 기능을 수행한다. 예를 들어 이벤트 핸들의 경우에는 이벤트가 셋(SET)될 때까지 기다렸다가 번환하며, 프로세스나 쓰레드의 경우에는 종료할 때까지 기다렸다가 반환한다. <리스트 1>처럼 main() 함수의 끝부분에 WorkerThread가 종료할 때까지 기다리는 코드를 볼 수 있을 것이다.
CRT를 올바르게 사용하자
CRT(C Run-Time Library)는 우리가 처음 C++ 언어를 배울 때부터 사용하던 많은 함수와 매크로가 포함되어 있다. 대표적인 printf() 함수부터 시작해서 메모리를 할당/해제 함수, 전역 변수의 생성자를 호출해 주는 기능까지 모두가 CRT의 기능이다.
<화면 1> CRT의 종류를 보여주는 등록정보 창
<리스트 2> CRT의 사용 예 |
#include #include #include using namespace std DWORD WINAPI WorkThread(LPVOID); void main() { // Result too large(34) 에러를 발생시킨다. pow( INT_MAX, INT_MAX); // 메인 쓰레드의 최근 에러 번호 출력 cout << "Main : " << errno << endl // 작업 쓰레드 시작 DWORD dwID HANDLE h = CreateThread(NULL, 0, WorkThread, NULL, 0, &dwID); // 종료 처리 WaitForSingleObject(h, INFINITE); CloseHandle(h); } DWORD WINAPI WorkThread(LPVOID) { // 작업 쓰레드의 최근 에러 번호 출력 cout << "WorkThread : " << errno << endl return 0 } // 결과 1 : 다중 쓰레드 버전의 CRT와 링크한 경우 Main : 34 WorkThread : 0 // 결과 2 : 단일 쓰레드 버전의 CRT와 링크한 경우 Main : 34 WorkThread : 34 |
이 CRT는 lib나 dll의 형태로 프로그램에 링크되는데, 성능상의 이유로 인해 다중 쓰레드 버전과 단일 쓰레드 버전으로 나눠져 있다. 다중 쓰레드 버전에서는 단일 쓰레드 버전과는 달리 동기화 코드가 추가되어 있는데, 프로그램이 단일 쓰레드만 가지고 있다면 굳이 이런 동기화 코드로 인한 부담을 껴안을 필요는 없다. 반면에 다중 쓰레드를 사용하는 경우에는 이런 동기화 코드가 반드시 필요하다. 즉, 자신의 쓰레드 사용 여부에 맞는 버전의 CRT를 링크할 필요가 있다는 것이다. <화면 1>과 <화면 2>는 비주얼 스튜디오 닷넷에서 CRT의 종류를 고를 수 있는 등록정보 창과 CRT의 종류를 보여주고 있다.
<리스트 2>는 CRT를 올바르게 선택하는 것이 중요하다는 사실을 보여주기 위해 준비한 예제다. GetLastError() API와 비슷하게 CRT에도 현재 쓰레드의 최근 에러 값을 보관하고 있는 errno 변수가 존재한다. <리스트 2>를 단일 쓰레드 버전의 CRT와 링크한 경우에는 작업 쓰레드의 최근 에러 값이 엉뚱하게 나오는 것을 확인할 수 있다.
마이크로소프트에서 제공하는 CRT에는 _beginthread와 _beginthreadex 두 가지의 쓰레드 생성 함수가 있다. 이 함수들이 성공적으로 호출되면 정수 값을 반환하는데, 타입은 다르지만 쓰레드의 핸들이다. _beginthread를 사용한 경우에 생성된 쓰레드에서는 _endthread를 호출해 자기 자신을 종료시킬 수 있는데 이때는 자동으로 CloseHandle()을 호출해 쓰레드 핸들을 닫아준다. 반면에 _beginthreadex와 _endthreadex를 사용한 경우에는 직접 CloseHandle()을 호출해 줄 필요가 있다. 물론 MSDN에 잘 설명되어 있지만 모르고 지나치기 쉬운 부분이니 주의해서 사용하길 바란다.
쓰레드 간의 데이터 전송
이번에는 쓰레드 간에 데이터를 전송하는 시나리오를 상상해 보자. 우리 주변에서 찾아 볼 수 있는 아주 흔한 예로는 동영상 파일을 읽어서 화상을 출력하는 것이 있다. 첫 번째 쓰레드(A)는 동영상 파일에서 특정 단위만큼씩 읽어서 다음 쓰레드(B)로 보내는 일만 열심히 한다. 다음 쓰레드(B)는 받은 데이터를 디코딩해 그 다음 쓰레드(C)로 보내는 일만 열심히 한다. 마지막 쓰레드(C)는 받은 데이터를 화면에 출력하는 일만 열심히 한다. 일반적으로 파이프라인이라고 부르는 작업 방식과 동일한 것이다.
다만 하드웨어 상에서의 파이프라인은 실제로 동시에 실행될 수 있어서 확실한 성능 향상을 가지고 오지만, 쓰레드를 사용한 파이프라인은 결국 한 CPU가 컨텍스트 스위칭을 해가면서 구현하는 것이기 때문에 I/O 연산이 연관된 경우에만 성능 향상을 기대할 수 있다. 파일에서 데이터를 읽는 것이나 화면에 이미지를 출력하는 것 모두 I/O 연산이기 때문에 동영상 파일을 읽어서 화상을 출력하는 경우는 다중 쓰레드를 적용하기에 아주 좋은 후보라고 볼 수 있다. 다이렉트X SDK에는 멀티미디어 데이터와 관련된 다이렉트쇼 SDK를 포함하고 있으며 실제로 다이렉트쇼는 이와 같은 방식으로 동작하고 있다.
이 시나리오에서 관심을 가지고 생각해 볼 부분은 데이터를 전송하는 방법이다. 쓰레드 간에는 함수 호출과 같은 방식으로 데이터를 넘겨주는 것이 불가능하다. 함수 호출을 해보았자 결국은 자신의 쓰레드이기 때문이다. 별도의 버퍼를 두어서 그것을 공유하는 방법이 일반적이다. 앞서 예를 든 쓰레드 A가 동영상 파일의 일부를 버퍼에 복사해 두면 쓰레드 B가 그 버퍼에서 읽어서 작업하는 방식이다(<그림 3-a>).
그러나 쉽게 예상할 수 있듯이 쓰레드 B가 버퍼에서 데이터를 읽는 도중에 쓰레드 A가 버퍼에 데이터를 쓰게 되면 쓰레드 B는 망가진 데이터를 읽게 된다. 그렇기 때문에 이 버퍼는 CriticalSection이나 Mutex와 같은 동기화 객체를 사용해 보호될 필요가 있다(CriticalSection이 더 적은 오버헤드를 갖기 때문에 프로세스간의 동기화가 아니라면 CriticalSection을 쓰는 것이 일반적이다).
이렇게 동기화를 추가한 후에는 한 쓰레드가 버퍼에 작업 중인 동안에 다른 쓰레드가 작업할 수 없는 문제가 생긴다. 다른 쓰레드의 작업을 기다리는 만큼의 시간을 손해보는 것이다. 이런 문제를 해결하기 위해서는 버퍼를 두 개로 늘리는 방법을 사용하면 된다(보통 이중 버퍼링이라고 불린다)(<그림 3-b>). 버퍼가 두 개로 늘어나면 한 쓰레드가 한 쪽 버퍼에 작업하는 동안에도 다른 쓰레드가 다른 쪽 버퍼에 작업할 수 있기 때문에 기다리는 시간을 줄일 수 있다. 물론 이런 경우에도 문제는 남아 있다. 두 쓰레드가 언제나 맞아 떨어지게 작업하는 것은 아니기 때문이다. 예를 들어 순간적으로 쓰레드 B의 작업이 지체되면 두 개의 버퍼가 다 차버리고, 쓰레드 A는 다음 번 데이터를 버퍼에 쓰기 위해 쓰레드 B가 버퍼의 내용을 읽어가기를 기다려야 하기 때문이다.
이중 버퍼링의 장단점
이러한 문제점을 해결하기 위한 방법으로 다수 개의 버퍼를 사용하는 방법을 생각할 수 있다. 익히 알고 있는 큐(Queue)가 바로 그것이다. 그런데 큐를 사용한다고 하더라도 크게 두 가지의 정책을 세울 수가 있다. 하나는 최대 버퍼의 수를 제한하는 방식이고 다른 하나는 무한으로 버퍼의 수를 증가시키는 방식이다. 두 가지 방식은 각각 장단점을 가지고 있다. 우선 최대 버퍼의 수를 제한하는 방식의 장점은 무한으로 메모리를 요구하는 것을 막을 수 있다는 점이다.
또한 버퍼의 수가 한계에 도달해 쓰레드 A가 중단된 상태로 기다리는 동안 CPU에 그만큼의 여유가 생겨서 쓰레드 B가 훨씬 많은 CPU 자원을 사용할 수 있게 된다. 그렇게 되면 자동으로 쓰레드 B가 큐에 쌓인 데이터를 읽어서 처리하는 속도가 빨라지고 프로그램의 전체적인 부하가 해소된다. 무한으로 버퍼를 제공하는 경우에는 쓰레드 B가 소화하지 못할 만큼의 데이터가 계속해서 큐에 쌓이므로 프로그램이 원활하게 동작하지도 않을 뿐더러 결국에는 메모리가 부족해 작업을 계속 할 수 없게 될 수도 있다. 물론 쓰레드 B가 I/O 요청을 기다린다거나 특정 조건을 기다리는 등의 이유로 작업이 정체되는 경우라면 쓰레드 A가 중단되는 것이 도움이 되지는 않을 것이다.
최대 버퍼의 수를 제한하는 경우에도 버퍼의 수가 한계에 도달했을 때의 처리 방식에 따라 크게 두 가지 정책을 세울 수가 있다. 하나는 여유분의 버퍼가 생길 때까지 쓰레드 A가 중단되어 기다리는 방식이며, 또 다른 하나는 버퍼 중에 하나를 강제로 비워버리는 방식이다. 선택의 기준은 달성하고자 하는 목적과 관계가 있다. 예를 들어 지금 사용하고 있는 예제인 동영상의 경우에, 그 중에서도 화상인 경우에는 버퍼 중에 하나를 버리는 방식을 사용하기에 적당하다.
화상의 경우에는 중간에 몇 프레임이 없어져도 사용자에게 크게 불편함을 주지 않기 때문이다. 또한 화상의 디코딩은 CPU를 많이 소모하는 작업이기 때문에 프로그램의 부하를 낮추는데도 많은 도움을 준다. 반면에 음성인 경우에는 버퍼 중에 하나를 버리는 방식을 사용하기에 적절하지 않다. 음성 데이터 한 프레임이 버려진다는 것은 영화의 배경음악이 끊긴다거나 주인공의 말소리가 잘리는 결과를 가져오기 때문에 화상에 비해 크게 거슬리게 된다. 물론 이러한 것들은 필자가 주로 하던 작업과 관련된 아주 단편적인 예이며 여러분의 환경에 맞는 적당한 정책을 수립할 필요가 있을 것이다.
이렇게 큐를 사용해 쓰레드간의 데이터 전송을 구현할 때 데이터를 매번 큐 내부의 버퍼에 복사하고, 복사해 오는 작업은 많은 부하를 발생시킨다. 그래서 큐에는 단순히 포인터만 유지하는 것이 일반적이다. 버퍼를 매번 생성하고 소멸시키는 것도 시간을 소모하는 작업이므로 미리 버퍼를 만들어 두어서 버퍼 풀(buffer pool)에 넣어두고 꺼내 쓰는 방법도 일반적이다. <그림 4>를 보면서 살펴보자. 쓰레드 A는 버퍼 풀에서 버퍼를 하나 얻어서 동영상의 일부를 읽는다. 그리고는 해당 버퍼의 포인터를 큐에 넣는다. 쓰레드 B는 큐에서 버퍼의 포인터를 얻어서 디코딩 작업을 한다. 사용이 끝난 버퍼는 다시 버퍼 풀로 반환해서 재사용할 수 있도록 한다.
<그림 4> 버퍼 풀의 사용
UpdateWindow() API
이번 기사의 마무리를 장식할 주제는 UpdateWindow()라는 API이다. 이 API는 윈도우 프로그래밍을 처음 시작할 때부터 등장한다. 기본적인 WinMain() 함수의 구현에서 CreateWindow()와 메시지 루프 사이에 위치한다. 처음 공부할 때 윈도우가 바로 화면에 그려지게 하는 역할을 한다고 배웠지만 UpdateWindow() 를 호출하지 않더라도 윈도우는 잘 그려졌기 때문에 의아해 하면서 넘어갔던 기억이 난다. 하지만 올해 초에 GUI 환경에서 단일 쓰레드를 사용하는 경우에 UpdateWindow()를 매우 유용하게 사용할 수 있음을 알게 되었다. 우선 <화면 3>을 살펴보자.
‘Test’ 버튼을 누르면 텍스트 박스에 1부터 100까지의 정수를 차례로 넣는 예제이다. 이 때 UpdateWindow()를 사용할 것인지 결정할 수 있는 옵션을 가지고 있는데, 그에 따라 서로 다른 결과를 보여준다. 옵션을 체크한 경우에는 예상대로 1부터 100이 차례로 보여지지만 체크하지 않은 경우에는 한참 후에 100만 보여진다.
코드를 보면 알 수 있지만 UpdateWindow()가 에디트 컨트롤이 바로 바로 자신을 다시 그리게 해주기 때문이다(이 예제는 MFC 프로젝트이다). SetWindowText()는 에디트 컨트롤의 내용을 바꾸고 WM_PAINT를 발생시키는 역할을 하지만 그 메시지는 바로 처리될 수 없다. OnBnClickedBtnTest() 함수는 메인 쓰레드에서 실행되기 때문에 이 함수가 완료될 때까지 메시지 루프가 실행될 수 없고, 그에 따라 WM_PAINT가 윈도우 프로시저에 전달될 수 없기 때문이다. 그러나 UpdateWindow()를 호출해 주면 WM_PAINT 메시지를 바로 윈도우 프로시저에 전달해 주기 때문에 에디트 컨트롤에 바로 그려질 수 있다.
void CUpdateWindowDlgnBnClickedBtnTest() { TCHAR temp[16]; CWaitCursor wait UpdateData(TRUE); // 천천히 숫자를 증가시킨다. for (int i = 0 i <= 100 ++i) { m_edit.SetWindowText( _itot( i, temp, 10) ); // 옵션에 따라 UpdateWindow() 호출 if (m_bUseUpdateWindow) m_edit.UpdateWindow(); // 테스트 용도 Sleep(20); } } |
필자가 UpdateWindow()를 사용하기 전에는 컨트롤이 바로 바로 갱신될 수 있도록 하기 위해 별도의 작업 쓰레드를 생성해 사용했다. 그러나 앞서 설명한 반응성이 중요하지 않다면 단일 쓰레드를 사용할 것을 권장하고 싶다. 이 기사를 제대로 읽은 독자라면 그 이유를 알 수 있을 것이라 생각한다. 쓰레드를 즐겨 사용하던 필자는 UpdateWindow()를 사용해 같은 문제를 해결할 수 있다는 점을 알았을 때 많은 교훈을 얻을 수 있었다. 그것이 어떤 종류의 교훈이었을지는 여러분의 상상에 맡기도록 하겠다.
소스 코드를 통해 배워라
필자는 다른 사람의 소스 코드를 분석하는 것을 좋아하지 않는다. MSDN 라이브러리나 잡지, 책을 읽는 것이 더 쉽고, 더 많은 지식을 준다고 생각하기 때문이다. 처음에도 말했듯이 필자는 이번 학기에 오픈소스 프로젝트 하나를 분석하고 있는데, 이 과정에서 얻은 교훈이 한 가지 있다. 나쁜 책을 통해 배울 수 있는 것보다는 나쁜 소스 코드를 통해 배울 수 있는 것이 더 많다는 점이다. 자신의 결점보다는 다른 사람의 결점이 더 잘 보이는 것이 사람이기 때문인 것 같다. 이 기사에서 부족한 점을 많이 느꼈다면 다음 번에 여러분이 기사를 쓸 때 좋은 거름이 될 거라는 생각으로 이해를 구한다.
출처: http://mareshoes.tistory.com/entry/퍼옴-다중쓰레드와-C [MARE SHOES]
=======================
=======================
=======================
출처: http://mooneegee.blogspot.kr/2015/01/oscritical-section-1-critical-section.html
[OS]임계영역(Critical Section) 접근 동기화1 - critical section 기반의 동기화 기법
쓰레드와 임계영역을 통해서 쓰레드가 메모리 영역을 어떻게 공유하는지, 공유한 메모리에 동시 접근할 수 있기 때문에 발생할 수 있는 잠재적인 문제에 대해서 알아보았다.
이를 바탕으로 임계영역의 동시접근을 막는 방법들을 알아보고자 한다.
참고로 아래의 내용은 WIndows system을 기반으로 설명하고 있다.
임계영역과 동기화
흔기 동기화라고 그러면 어떤 것들을 일치시켜주는 것을 의미한다. 클라우드와 로컬 저장소 사이의 동기화가 실행되면 클라우드에 저장된 데이터와 로컬 저장소에 저장된 데이터가 일치하지 않는가. 그런데 여기서 동기화는 조금 다른 의미를 지니고 있다. 일치한다는 의미가 아니라 순서에 있어서 질서가 잘 지켜지고 있다는 의미이다.
질서가 잘 지켜지고 있다는 말은 임계영역에 접근하는 쓰레드의 순서가 잘 지켜진다는 것을 뜻한다. 동시접근하면 발생하는 문제를 예방하기 위해 동시접근을 예방하는 동시에 한 번에 하나의 쓰레드만 임계영역에 접근할 수 있다록 하는 것이 바로 임계영역 접근 동기화이다.
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0; // thread는 전역변수를 공유한다.
void IncreaseCount()
{
gTotalCount++; // critical section
}
unsigned int WINAPI ThreadProc( LPVOID lpParam )
{
for(DWORD i=0; i<1000; i++)
{
IncreaseCount();
}
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadId[NUM_OF_GATE];
HANDLE hThread[NUM_OF_GATE];
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
hThread[i] = (HANDLE)
_beginthreadex (
NULL,
0,
ThreadProc,
NULL,
CREATE_SUSPENDED, // thread가 생성되고 바로 실행되지 않고 대기
(unsigned *)&dwThreadId[i]
);
if(hThread[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
ResumeThread(hThread[i]); // 대기 중인 thread를 실행시킨다.
}
WaitForMultipleObjects(NUM_OF_GATE, hThread, TRUE, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
CloseHandle(hThread[i]);
}
return 0;
}
<출처: 윤성우 저, '윈도우즈 시스템 프로그래밍'>
위의 코드는 전역변수를 선언하였고, 쓰레드들이 그 전역변수에 임의로 접근하는 코드이다. 전에도 말했지만 쓰레드의 실행순서를 예측하는 것은 불가능하다. ResumeThread() 함수를 통해 hThread[0] ~ hThread[5]를 거의 동시에 실행시키므로, IncreaseCount() 함수 내부의 gTotalCount에 하나씩 차례대로 접근할 것이라고 오해해서는 안된다. 쓰레드는 공유하는 메모리 영역에 자유롭게 접근할 수 있기 때문이다. 우리는 total count가 6000이 나올 것이라고 예상하지만, 6000이 나오지 않을 수도 있다.
임계영역에 대한 간단한 이해
임계영역에 접근하려는 3개의 쓰레드가 있다. 고양이가 바로 쓰레드이다.
3개의 쓰레드들이 동시에 임계영역에 접근하려고 한다. 하지만 이는 알고 있듯이 문제가 발생한다. 그래서 하나의 쓰레드만 임계영역에 접근할 수 있다. 하나의 쓰레드가 임계영역에 진입하면 다른 쓰레드들은 기다린다. 위 그림에서는 쓰레드들이 잠을 자는 것으로 그려놓았는데, 실제로 BLOCK 상태가 된다.
먼저 들어간 쓰레드가 임계영역을 빠져나오면, 잠자던(BLOCK 상태) 쓰레드 중에 한 쓰레드가 임계영역에 진입할 수 있다. 진입하지 못한 쓰레드는 계속 잠을 자게 된다.
대략적으로 임계영역을 동기화 하는 방법을 알았다. 이제 코드에 적용해보자.
임계영역 기반의 동기화 기법
critical section은 임계영역을 나타내는 용어인 'critical section'를 그대로 기법의 이름으로 사용하였다.
먼저 critical section 기반의 동기화 기법을 사용한 코드를 보자.
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>
#define NUM_OF_GATE 6
LONG gTotalCount = 0; // thread는 전역변수를 공유한다.
CRITICAL_SECTION hCriticalSection; // critical section 선언
void IncreaseCount()
{
gTotalCount++; // critical section
}
unsigned int WINAPI ThreadProc( LPVOID lpParam )
{
for(DWORD i=0; i<1000; i++)
{
EnterCriticalSection(&hCriticalSection); // enter
IncreaseCount();
LeaveCriticalSection(&hCriticalSection); // leave
}
return 0;
}
int _tmain(int argc, TCHAR* argv[])
{
InitializeCriticalSection(&hCriticalSection); // critical section 초기화
DWORD dwThreadId[NUM_OF_GATE];
HANDLE hThread[NUM_OF_GATE];
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
hThread[i] = (HANDLE)
_beginthreadex (
NULL,
0,
ThreadProc,
NULL,
CREATE_SUSPENDED, // thread가 생성되고 바로 실행되지 않고 대기
(unsigned *)&dwThreadId[i]
);
if(hThread[i] == NULL)
{
_tprintf(_T("Thread creation fault! \n"));
return -1;
}
}
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
ResumeThread(hThread[i]); // 대기 중인 thread를 실행시킨다.
}
WaitForMultipleObjects(NUM_OF_GATE, hThread, TRUE, INFINITE);
_tprintf(_T("total count: %d \n"), gTotalCount);
for(DWORD i=0; i<NUM_OF_GATE; i++)
{
CloseHandle(hThread[i]);
}
DeleteCriticalSection(&hCriticalSection); // critical section 리소스반환
return 0;
}
<출처: 윤성우 저, '윈도우즈 시스템 프로그래밍'>
EnterCriticalSection() 함수와 LeaveCriticalSection() 함수를 이해하면 critical section 기반의 동기화 기법은 끝난다. 먼저 EnterCriticalSection() 함수는 인자로 CRITICAL_SECTION의 핸들(handle)을 사용한다.
옷가게의 fitting room을 떠올려보자. 내가 옷을 골라서 갈아입기 위해 fitting room에 들어가려고 한다. 하지만 이미 fitting room을 사용하는 사람이 있다. '사용중'이라는 표시가 보인다. 그러면 나는 fitting room을 사용할 수 없고, 지금 사용 중인 사람이 사용을 마치기를 기다려야 한다. 그 사람이 사용을 마치고 나오면 이제 내가 사용할 수 있다. 내가 fitting room에 들어가는 동시에 fitting room은 다시 '사용중' 상태가 된다.
임계영역이 바로 fitting room에 해당하고, critical section 기반의 동기화 기법이 바로 '사용중'을 표시해서 다른 이들이 fitting room에 두 명이 동시에 들어가는 것을 막는 방법이다.
한 쓰레드가 EnterCriticalSection() 함수를 실행하면 fitting room에 들어가 사용 중이라는 선언한다. 그러면 다른 thread에 의해 EnterCriticalSection() 함수가 호출되면 이미 fitting room이 사용 중이기 때문에 이 쓰레드는 BLOCK 상태가 된다. 만약 fitting room을 사용 중인 쓰레드가 사용을 마치면 BLOCK 상태의 쓰레드가 비로소 BLOCK 상태에서 빠져나와 fitting room에 들어가 사용할 수 있다.
LeaveCriticalSection() 함수는 어떤 쓰레드가 fitting room을 빠져나올 때, 다른 쓰레드들이 fitting room을 사용할 수 있도록 해주기 위한 함수이다. fitting room 사용을 마치고 LeaveCriticalSection() 함수를 호출하면, EnterCriticalSection() 함수를 호출했다가 BLOCK 상태가 된 쓰레드가 있다면 그 쓰레드는 BLOCK 상태에서 빠져나와서 임계영역에 진입하게 된다.
정리하면 이 기법은 임계영역에 한 쓰레드만 진입할 수 있도록 다른 쓰레드를 BLOCK 상태로 만든다. 그리고 한 쓰레드가 임계영역을 빠져나오면 다른 쓰레드가 BLOCK 상태에서 빠져나와 임계영역에 진입한다.
=======================
=======================
=======================
출처: http://lateafternoon.tistory.com/234
원문: http://sol9501.blog.me/70104141478
|소켓 프로그래밍| 멀티 스레드(스레드 동기화) ③
* 이번 포스팅은 멀티스레드를 사용하는 경우 두 개 이상의 스레드가 공유 데이터를 접근하는 경우 발생하는 문제를 알아보고
이를 해결하고자 사용하는 동기화 기법인 임계 영역, 뮤텍스, 이벤트, 세마포어등을 알아보겠습니다.
1. 동기화의 필요성
- 다음의 경우 멀티스레드 사용시 스레드 동기화가 필요하다.
● 두개 이상의 스레드가 공유 리소스를 접근할때, 오직 한 스레드만 접근을 허용해야 하는 경우.
● 특정 사건 발생을 다른 스레드에게 알리는 경우. 예를 들면, 한 스레드가 작업을 완료한후,
대기중인 다른 스레드를 깨우는 경우
간단히 다음의 그림 1을 보면서 설명하겠습니다.
그림 1) 스레드 동기화
- 위의 그림 1과 같은 경우 Thread1과 Thread2는 동시에 공유 변수 5에 접근하고 있습니다. 이 경우 Thread1은 공유 변수 5를 읽어 들여 1을 더하는 작업을 하고 그 결과 값 6을 공유 변수에 다시 쓰고 있습니다. Thread2도 공유 변수 5를 가져와 +1을 하고 그 값을 다시 공유 변수에 쓰고 있습니다. 이 경우 유저는 양쪽의 스레드에서 모두 +1을 했으니 +7의 값을 기대하고 있을 것 입니다. 즉 실제적으로 유저가 기대한 그림은 다음 그림 2와 같은 시나리오 일 것입니다.
그림 2) 동기화 적용
- 위와 같이 공유 변수에 대한 접근을 하나의 스레드로 제한 함으로써 정확한 값을 리턴 받을 수 있습니다. 즉 이와 같이 멀티스레드 환경에서 발생하는 문제를 해결하기 위한 일련의 작업을 스레드 동기화(Thread Synchronization)라 부릅니다. 윈도우는 다양한 동기화 관련 API를 제공하여 프로그래머가 상황에 따라 적절한 동기화 방법을 선택할 수 있도록 하고 있습니다. 이러한 스레드 동기화 기법은 다음 표1과 같습니다.
[표 1] - 스레드 동기화 기법
종류 | 용도 |
임계 영역(critical section) | * 공유 리소스에 대해 오직 하나의 스레드 접근만 허용한다.(한 프로세스에 속한 스레드에만 사용 가능) |
뮤텍스(mutex) | * 공유 리소스에 대해 오직 하나의 스레드 접근만 허용한다.(서로 다른 프로세스에 속한 스레드에도 사용 가능) |
이벤트(event) | * 특정 사건 발생을 다른 스레드에 알린다. |
세마포어(semaphore) | * 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근을 제한한다. |
* 동기화에 대한 더 자세한 내용은 제프리 리처의 "WINDOWS VIA C/C++"나 정덕영 저 "윈도우 구조와 원리"를 추천합니다.
2. 임계영역
- 임계역역은 두 개 이상의 스레드가 공유 리소스를 접근할 때 오직 한 스레드 접근만 허용해야 하는 경우에 사용한다. 임계 역역은 스레드 동기화를 위해 사용하지만 동기화 객체로 분류하지 않으며 특징 또한 다르다. 대표적인 특징은 다음과 같습니다.
● 임계 영역은 유저 영역 메모리에 존재하는 구조체다. 따라서 다른 프로세스가 접근 할 수 없으므로 한 프로세스에 속한 스레드 동기화에만 사용할 수 있다.
● 일반적인 동기화 객체보다 빠르고 효율적이다.
임계 영역을 사용을 확인하기 위해 간단한 소스를 작성해 보겠습니다.
more..
- 위의 소스를 그대로 실행하면 hThread[0]과 hThread[1]이 공유 변수 val을 한 번씩 점유해 hThread[0]은 앞에서부터 채워나가고 hThread[1]은 뒤에서부터 값을 채워나가니 결과적으로 배열에는 값 9, 3 이 대략 반반씩 채워지게 됩니다. 결과화면은 그림 3과 같습니다.
그림 3) 일반적인 실행화면
- 그렇다면 이번에는 임계 영역을 사용하여 하나의 스레드만이 공유 리소스를 점유하도록 해보겠습니다.
우선 CRITICAL_SECTION 구조체 변수를 전역 변수를 선언합니다. |
임계 영역을 사용하기 전에 InitializeCriticalSection()함수를 호출하여 초기화 합니다. |
공유 리소스를 사용하기 전에 EnterCriticalSection() 함수를 호출합니다. 공유 리소스를 사용하고 있는 스레드가 없다면 EnterCriticalSecrion() 함수는 곧바로 리턴합니다. 공유 리소스를 사용하고 있는 스레드가 있다면 EnterCriticalSecrion() 함수는 리턴하지 않고 해당 스레드는 대기 상태가됩니다. |
공유 리소스 사용이 끝나면 LeaveCriticalSection() 함수를 호출합니다. |
임계 영역 사용이 끝나면 DeleteCriticalSection() 함수를 호출합니다. |
실행화면은 다음 그림 4와 같습니다. 소스는 위의 소스 1의 주석부분만 풀어주시면 됩니다.
그림 4) 임계 영역사용
3. 이벤트
- 이벤트는 특정 사건의 발생을 다른 스레드에 알리는 경우에 주로 사용합니다. 서두에 동기화의 필요성에서
● 특정 사건 발생을 다른 스레드에게 알리는 경우. 예를 들면, 한 스레드가 작업을 완료한 후, 대기중인 다른 스레드를 깨우는 경우
동기화가 필요하다고 했습니다. 즉 한 스레드가 작업을 완료한 후 대기 중인 다른 스레드가 깨어나서 진행하는 시나리오를 만들 때 이벤트를 이용합니다. 이벤트의 동기화의 간단한 과정은 다음과 같습니다.
1. 이벤트를 비신호 상태로 생성한다. |
2. 한 스레드가 작업을 진행하고 나머지 스레드는 이벤트에 대해 Wait*()함수를 호출 함으로써 이벤트가 신호 상태가 되기를 기다린다. |
3. 스레드가 작업을 완료하면 이벤트를 신호 상태로 바꾼다. |
4. 기다리고 있던 모든 스레드가 깨어나서 작업을 진행한다. |
- 이벤트는 대표적인 동기화 객체로 신호와 비신호라는 두 가지 상태를 가지며 상태를 변경 할 수 있도록 다음과 같은 함수를 제공한다.
1) SetEvent
형식 : Bool SetEvent(HANDEL hEvent); |
비신호 상태 - > 신호 상태 |
2) ResetEvent
형식 : Bool ResetEvent(HANDEL hEvent); |
신호 상태 - > 비신호 상태 |
3) CreateEvent
형식 : HANDLE WINAPI CreateEvent( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in BOOL bManualReset, __in BOOL bInitialState, __in_opt LPCTSTR lpName ); |
||
파라미터 | ||
lpEventAttributes | * SECURITY_ATTRIBUTES 구조체 변수의 주소값을 대입한다. SECURITY_ATTRIBUTES 구조체는 핸들 상속과 보안 디스크립터 정보를 전달하는 용도로 사용된다. 만약 이 값이 NULL이면 핸들은 상속되어 질 수 없다. | |
bManualReset | * TRUE이면 수동 리셋 이벤트, FALSE이면 자동 리셋 이벤트가 생성된다(표 1참고). | |
bInitialState | * TRUE이면 신호 상태로, FALSE이면 비 신호 상태로 시작한다. | |
lpName | 이벤트를 서로 다른 프로세스에 속한 스레드가 사용(공유)할 수 있도록 이름을 줄 수 있습니다. NULL을 사용하면 이름 없는 이벤트가 생성됩니다. | |
리턴 값 | ||
성공 | 이벤트 핸들 | |
실패 | NULL |
이벤트는 다음과 같은 종류가 있다.
[표 1] - 이벤트 특성에 따른 종류
종류 | 특징 |
자동 리셋 이벤트 | * 이벤트를 신호 상태로 바꾸면 기다리는 스레드 중 하나만 깨운 후 자동으로 비신호 상태가 된다. 따라서 자동 리셋 이벤트에 대해서는 ResetEvent() 함수를 사용할 필요가 없다. |
수동 리셋 이벤트 | * 이벤트를 신호 상태로 바꾸면 계속 신호 상태를 유지하므로 결과적으로 기다리는 스레드를 모두 깨우게 된다. 자동 리셋 이벤트와 달리 이벤트를 비신호 상태로 바꾸려면 명시적으로 ResetEvent 함수를 호출해야 한다. |
- 다음의 예제는 마스터 스레드가 버퍼에 값을 쓸 동안 다른 스레드들의 사용을 방지하고 있습니다. 첫 째 마스터 스레드는 비신호와 수동 리셋(manual-reset) 이벤트로 이벤트 오브젝트들을 생성해 초기에 비신호 상태를 유지합니다. 그 후 프로그램에서 요구하는 리더 스레드들을 생성하고 마스터 스레드는 쓰기 작업을 실행합니다. 마스터 스레드의 쓰기 작업이 끝나면 이벤트 오브젝트들은 신호상태로 바뀌고 쓰기 작업을 실행합니다. 실행 화면은 그림 5와 같습니다.
아래 쓰이는 모든 함수들은 멀티 스레드 포스팅에서 모두 다루었던 함수들입니다. 만약 이해가 되지 않으시면 다시 한 번 앞의 포스팅들을 참고 하시길 바랍니다.
그림 5) 이벤트 예제
이벤트 소스
원본 소스: http://msdn.microsoft.com/en-us/library/ms686915%28v=VS.85%29.aspx
출처: http://lateafternoon.tistory.com/234 [지행프로젝트]
=======================
=======================
=======================
출처: http://skmagic.tistory.com/entry/%EC%8A%A4%EB%A0%88%EB%93%9C-%EB%8F%99%EA%B8%B0%ED%99%94
1. 스레드 동기화
0) 동기화란?
-작업들 사이의 수행 시기를 맞추는 것.
사건이 동시에 일어나거나, 일정한간격을 두고 일어나도록 시간의 간격을 조정하는 것을 이른다.
1) 스레드 동기화(thread synchronization) 필요성
- 멀티 스레드를 사용하는 프로그램에서 두 개 이상의 스레드가 공유 데이터를 접근하는 경우
2) 다양한 스레드 동기화 기법
3) 스레드 동기화 원리
교착상태에 대해서 알아보자
[프로그래밍/NetWork] - 교착상태 (DeadLock)
그리고
이제 각종 동기화 기법에대해서 하나씩 자세히 알아보자!!!!
2. 임계 영역(CriticalSection)
- 두 개 이상의 스레드가 공유 리소스를 접근할 때, 오직 하나의 스레드 접근만 허용해야 하는 경우에 사용
2_1)특징
- 유저영역 메모리에 존재하는 구조체이므로 한 프로세스에 속한 스레드 동기화에만 사용 가능
- 일반적인 동기화 객체보다 빠르고 효율적
2_2) 임계 영역 사용 예
3, 이벤트 객체
- 특정 사건 발생을 다른 스레드에게 알릴 때 주로 사용
3_1) 이벤트 객체를 이용한 동기화 예
- 이벤트 객체를 비신호 상태로 생성
- 한 스레드가 작업을 진행하고, 나머지 스레드는 이벤트 객체에 대해 Wait어쩌구()함수를 호출함으로써 이벤트 객체가 신호상태가 되기를 기다림
- 스레드가 작업을 완료하면, 이벤트를 신호 상태로 바꿈
- 기다리고 있던 모든 스레드가 깨어나서 작업을 진행
3_2) 이벤트 객체 상태 변경
3_3) 이벤트 객체의 종류
- 자동 리셋(auto-reset) 이벤트
- 수동 리셋(manual-reset) 이벤트
3_4) 이벤트 객체 생성
출저:Wanna be Special Software Engineer :: 네이버 블로그
출처: http://skmagic.tistory.com/entry/스레드-동기화 [자기계발을 멈추면 죽는다]
=======================
=======================
=======================
멀티 태스킹을 말할때 피할수 없는것이 동기화(Synchronization)문제이다.
동기화란 멀티 태스킹 환경에서 여러개의 처리(태스크)를 서로의 진행상태에 맞추어 진행시키는것을 말한다.
문제점 코드 (플래그를 사용한 배타적 제어 코드)
bool InUse = false;
int func(void)
{
....
while(InUse)
{
}
InUse = true; // 다른곳에서 접근 금지
File * fp = fopen("sample.dat", "wb");
if(fp != NULL)
{
......
fclose(fp);
}
InUse = false; // 다른곳에서 접근 허용
}
이 코드의 문제점은 첫째, InUse가 false인것을 확인하는 조작가 true로 조작 하는 사이에 비록 짧지만 시간간격이잇다.
이처럼 얼마되지 않은 시간에 다른 쓰레드가 사용가능하다고 판단해서 리소스 접근이 진행될수있다.
비록 CPU가 하나밖에 없어도 어떤 쓰레드가 InUse를 체크한후에 다른쓰레드로 바뀌고 거기서 또다시 InUse가 체크될 가능성이 있다. 시간간격이 짧아지면 가능성은 줄어들지만 이러한 문제점을 내포할수가 있다.
왜냐하면 Windows에서는 어플리케이션 수준에서 쓰레드 전환을 금지할수 없기때문에 이문제를 막을수가 없다.
또다른 문제는 InUse가 false가 될때까지 대기하는 루프이다. 이 코드를 실행하면 InUse가 true인동안에는 고속으로 루프를 반복한다.
단지 대기만 하는것에 불과한테 쓸데없이 CPU를 소비하여 다른 프로그램이나 쓰레드에 영향을 미치게 된다.
이런루프를 비지웨이트(busy-wait)라고 하며 멀티쓰레드에서는 최대한 피해야 한다고 알려져있다.
유감스럽게도 C/C++언어수준에서는 이런문제를 해결할수 없다. 즉 동기화를 수행하기위해서는 Windows등 운영체제에서 지원이 필요하다.
크리티컬 섹션 자체는 Windows객체중에 하나며 프로그램에서 CRITICAL_SECTION 변수를 하나 선언해서 사용한다.
일반 Windwos 객체와 달리 프로세스 메모리 공간에 확보된 변수를 이용하기 때문에 동일 프로세스내의 쓰레드 동기화에 사용할수 있지만 다른 프로세스 동기화에는 사용할수없다.
객체를 사용해서 배타적제어를 한다는 말에 감이 오지 않은 사람도 잇을것이다.
쉽게 말해서 객체의 소유권이라고 생각하면 된다. 객체를 소유하는것은 쓰레드이다.
배타적 제어를 하려면 공유리소스에대해 크리티컬 섹션을 정의한뒤, 접근하고싶은 스레드는 우선 그 객체의 소유권을 시스템에게 요구한다.
소유권을 얻은 스레드는 리소스에 접근하고 그후에 소유권을 반납한다.
소유권은 하나뿐이므로 리소스에 접근할수 있는 쓰레드 역시 하나뿐이다.
크리티컬섹션을 사용할 경우에는 공유리소스에 접근 하는 부분의 코드를 정확히 EnterCriticalSection API와 LeaveCriticalSection API로 둘러싸는 형태가 된다. 둘러싸인 코드의 접근시 쓰레드간 경합을 일으킬 가능성이 있는 위험한 구역이므로 말그대로 크리티컬 섹션인것이다.
다른쓰레드에 소유권이 있어 획득할수없는 경우 EnterCriticalSection을 호출한 쓰레드는 대기상태로 들어가서 돌아오지 않는다.
그리고 소유권을 획득할수 잇는 상태가 되면 Windows는 자동적으로 쓰레드 실행을 재개해준다.
대기중인상태에서는 CPU파워를 소비하지 않기때문에 앞서와 같은 busyWait 문제가 발생하지 않는다.
EnterCriticalSection 대신 TryCriticalSection을 사용하면 소유권 획득여부와 관계없이 곧바로 복귀한다. 즉 이를 이용하면 소유권을 획득할수 잇을때까지 다른처리를 계속 할수있다.
CRITICAL_SECTION critisec;
int func(void)
{
EnterCriticalSection(&critisec);
File * fp = fopen("sample.dat", "wb");
if(fp != NULL)
{
......
fclose(fp);
}
LeaveCriticalSection(&critisec);
}
int main(void)
{
InitializeCriticalSection(&critisec);
// 쓰레드 생성
DeleteCriticalSection(&critisec);
return 0;
}
주의 해야할점은 보호해야할 리소스와 크리티컬섹션 사이에는 시스템과 아무런 관련이 없다는 점이다.
이는 예제에서 API를 호출할때 파일포인터나 파일명을 넘기지 않는 것을 보면 분명하게 알수있다.
따라서 위험영역을 올바르게 싸지 않아도 API 호출은 에러가 발생하지않는다.
다만 실행시에 쓰레드간 경합때문에 오동작 하는경우가 잇을 뿐이다. 이는 모든 동기화구조에서 공통된 점이다.
동기화 매커니즘을 올바르게 집어 넣는것도 프로그래머의 책임이다.
===================
크리티컬 섹션도 동기화 객체이지만 약간 구조가 달라서 구별되는 동기화 객체가 있다.
다음은 Windows에서 제공하는 주요 동기화 객체이다.
동기화개체 용도
1) 뮤텍스 리소스 배타적 제어
2) 세마포어 리소스를 동시에 사용할수 있는 개수 제어
3) 이벤트 다른 쓰레드에 이벤트 통지
4) 대기가능 타이머 타이머 이벤트 통지
크리티컬 섹션과의 큰 차이는 동기화 객체는 모두 프로세스 간의 동기화에 사용 할수 있다는 점이다.
이는 크리티컬 섹션 객체와 달리 동기화 객체의 실체가 Windows 내부에 확보되어 잇음을 의미한다.
프로그램에서는 그 핸들을 사용해 객체를 조작한다. 따라서 핸들을 얻을수만 있으면 어느 프로세스에서도 동기화 객체에 접근할수가
있다. 핸들 그자체를 프로세스 간에 주고받는것이 귀찮기는 하지만 (핸들값은 프로세스 내부에서만 유호하기때문에 다른프로세스에 값을 그냥 넘기면 이용할수없다) 동기화 객체에는 파일처럼 이름을 붙일수 잇으므로 이를 지정해서 동기화 객체의 핸들을 얻으면 된다.
또한 동기화객체는 객체의 시그널 비시그널 상태라는 속성을 이용해 동기를 얻는다는점이 다르다.
WaitForSingleObject / WaitForMultipleObject API를 이용하여시그널상태가 되는것을 감시한다.
뮤텍스는 Windows객체이므로 필요 없으면 삭제하는 편이 바람직하다. 이때는 파일객체나 프로세스 객체처럼 CloseHandle API를 사용해서 닫으면 된다. 단 닫아도 즉석에서 객체가 삭제되는 것은 아니라는 사실에 주의 해야한다.
동기화 객체는 여러개의 프로세스나 쓰레드에서 접근할 가능성이 있기 때문에 어디선가 닫았다고 해서 즉석에서 삭제되어야 한다면 나머지 프로세스 쓰레드에게는 곤란한 일이다. 그러므로 모든 핸들이 닫혓을때만 실제로 삭제된다.
이를 뒤집어 생각해보면 하나라도 닫히지 않은 핸들이 잇다는 말은 객체가 삭제되지않고 남는다는 사실을 의미한다.
이는 리소스가 낭비되므로 필요없다면 즉시 닫는 버릇을 들이자. 또한 파일이나 다른 Windows객체처럼 프로세스가 종료되면 사용중이던 핸들이 자동으로 닫힌다.
HANDLE hMutex;
int func(void)
{
WaitForSingleObject(hMutext, INFINITE);
FILE *fp= fOpen("Sample.dat","wb");
if(fp != NULL)
{
.....
fclose(fp);
}
ReleaseMutex(hMutex);
}
int main(void)
{
hMutex = CreateMutex(NULL, FALSE, "Sample_mutex");
//스레드 생성해서 func 호출
// 뮤텍스 닫기
CloseHandle(hMutex);
return 0;
}
유한개의 리소스 관리에는 세마포어를 사용한다.
세마포어는 뮤텍스와 비슷하지만 카운터를 관리한다는 점이 다르다.
세마포어는 관리하는 카운터값이 0이면 비시그날상태 1이상이면 시그날상태가된다. 대기용 API는 시그널상태에서 실행을 재개하는 동시에 세마포어의 카운트값은 1씩 줄어든다. 카운트가 0이되면 세마포어는 다시 비시그날상태로된다.
그리고 ReleaseSemaphore API로 카운터를 증가하면 다시 세마포어는 시그날상태로 된다.
이러한 세마포어를 이용하는 경우는 어떤인위적인 제한을 하고 싶을때 이용한다.
예를들어 성능을 확보하거나 메모리 사용량을 억제하기위해 데이타베이스나 웹서버에 접근하는 클라이언트수를 제한하고자 할때 이용하는 경우가잇다.
마지막으로 소개할 이벤트 객체는 뮤텍스나 세마포어와는 성잘이 좀 다르다. 소유권이나 카운터같은 리소스 접근 제어 속성이 없다.
그 대신 객체의 시그널상태를 프로그램에서 자유롭게 제어할수잇다. 즉 대기용API를 호출해서 정지한 스레드에대한 이벤트 객체를 시그널상태로 바꾸는 방법으로 실행을 재개 할수있다. 이 기능을 재개를 위한 이벤트 통지라고 생각해도 괜찮을것이다.
출처 : http://blog.naver.com/kkan22
출처: http://blog.pages.kr/99 [hi.pe.kr 날으는물고기·´″°³о♡]
=======================
=======================
=======================
출처: http://egloos.zum.com/tiger5net/v/5537603
동기화 객체
두 개 이상의 thread가 공유자원에 대해 동시에 접근할 때, 데이터의 일관성을 보장할 수 없기 때문에 이를 막기 위해 공유자원을 적절히 사용할 수 있게 하는 방법을 동기화라 한다.
동기화 객체에는 아래와 같이 user-mode 객체와 kernel-mode 객체로 나뉜다.
object | Linux/Unix | Windows |
File Lock | kernel-mode | kernel-mode |
† Critical Section | 없음 | user-mode 한 프로세스 내에서만 사용가능 |
Interlock | 없음 | user-mode 한 프로세스 내에서만 사용가능 |
† Mutex | POSIX 함수 한 프로세스 내에서만 사용가능 하나의 공유자원 동기화에 사용 |
kernel-mode 프로세스끼리도 사용가능 타임아웃기능 사용가능 하나의 공유자원 동기화에 사용 |
† Semaphore | kernel-mode POSIX 함수 프로세스끼리도 사용가능 다중 공유자원 동기화에 사용 |
kernel-mode 프로세스끼리도 사용가능 타임아웃기능 사용가능 다중 공유자원 동기화에 사용 |
Condition Variable | POSIX 함수 타임아웃기능 사용가능 다중 스레드를 동시에 깨울 수 있음 |
없음 |
† Event | 없음 | kernel-mode 프로세스끼리도 사용가능 타임아웃기능 사용가능 다중 스레드나 프로세스를 동시에 깨울 수 있음 |
여기서는 † 표시된 가장 많이 사용되는 객체들만 정리하기로 함.
0. 사전지식
※ User-mode 객체
커널 영역의 객체를 접근할 필요 없이 메모리 상에서 객체를 생성하여 운용하므로 연산이 빠르다는 장점이 있지만 프로세스 간의 동기화는 할 수 없다.
※ Kernel-mode 객체
커널 객체를 사용한 동기화는 signaled, nonsignaled 둘 중 하나의 상태로 존재한다.
mutex, semaphore, event의 경우 생성할 때에는 signaled-mode이지만, 대기함수인
WaitForSingleObject()나 WaitForMultipleObject()를 사용하여 nonsignaled-mode로 만들 수 있다.
- signaled
thread가 해당 객체에 접근이 가능하며, 접근한 thread는 객체를 사용 가능하다.
- nonsignaled
thread는 해당 객체에 접근할 수 없고 signaled될 때까지 대기한다.
※ 관련용어
- critical section
임계영역. 공유 자원을 접근하는 프로세스 내부의 코드 영역.
동기화 객체와 같은 단어지만 그 뜻은 다르다.
- deadlock
교착상태. 두 개 이상의 프로세스들이 더 이상 진행을 할 수 없는 상태.
- livelock
라이브락. 두 개 이상의 프로세스들이 다른 프로세스의 상태 변화에 따라 자신의 상태를 변화시
키는 작업만을 수행하고 있는 상태.
- mutual exclusion
상호배제. 한 프로세스가 공유 자원을 접근하는 임계영역 코드를 수행하고 있으면 다른 프로세스
들은 공유 자원을 접근하는 임계영역 코드를 수행 할 수 없는 조건.
- race condition
경쟁상태. 두 개 이상의 프로세스가 공유 자원을 동시 접근하려는 상태.
- starvation
기아. 특정 프로세스가 수행 가능한 상태임에도 불구하고 매우 오랜 기간 동안 자원을 사용하지
못하는 상태.
1. MUTEX
- 모든 thread에 사용되는 kernel-mode 동기화 객체.
- 공유자원에 대해 한 thread가 단독으로 점유하며 다른 thread의 접근을 차단.
뮤텍스 생성
HANDLE CreateMutex ( LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName );
뮤텍스 소유
HANDLE OpenMutex ( DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName );
뮤텍스 소유 해제
BOOL ReleaseMutex ( HANDLE hMutex );
뮤텍스 삭제
HRESULT CloseHandle ( HANDLE hHandle );
- 공유자원을 다 사용한 후 ReleaseMutex를 호출하여 다른 thread의 접근을 허용.
* critical section은 구조체의 값을 통해 잠금을 하지만 mutex는 객체를 생성하기 때문에 critical section보다 느리다.
* mutex name을 가지며 name은 유일한 값이 된다.
* mutex를 소유한 thread가 mutex의 소유를 반납하지 않고 비정상 종료될 경우 강제로 해제시켜 다른 thread에서 소유할 수 있도록 한다.
* mutex를 소유한 thread가 중복으로 호출할 경우 critical section처럼 진입을 허용하고 내부 count만 증가시켜 deadlock을 발생시키지 않는다.
* mutex는 다른 말로 상호배제 semaphore라고도 불리는데 이는 mutex가 특별한 형태의 binary semaphore이다. (일반 binary semaphore와 다른 이유는 소유권이 있다는 점이다)
2. SEMAPHORE
- mutex와 비슷하지만 접근 가능한 thread의 개수를 지정 가능한 kernel-mode 동기화 객체.
- 내부 count만큼 thread가 접근가능하며 count가 0이 되었을 때는 다른 thead의 접근을 차단.
- ReleaseSemaphore를 호출하여 count를 1 증가시켜 다른 thread의 접근을 허용
세마포어 생성
HANDLE CreateSemaphore ( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName );
세마포어 진입
HANDLE OpenSemaphore ( DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName );
세마포어 진입해제
BOOL ReleaseSemaphore ( HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount );
세마포어 삭제
HRESULT CloseHandle ( HANDLE hHandle );
* mutex와 마찬가지로 named semaphore는 프로세스 간 동기화도 가능
* thread나 process에 무관하게 진입 시 count를 증가
* 하나의 thread에서 모두 소유한 상태에서 다시 진입하면 deadlock이 발생
* binary semaphore
0이나 1의 값을 가지는 semaphore로 0이면 사용불가, 1이면 사용 가능하다.
* counting semaphore
내부 카운터를 사용하여 여러 차례 획득하고 반환할 수 있는 semaphore로 생성 시 초기 Token값을 지정할 수 있다.
3. CRITICAL SECTION
- 같은 프로세스 내에서만 사용될 수 있는 user-mode 동기화 객체.
- 공유자원에 대해 한 thread가 단독으로 점유하며 다른 thread의 접근을 차단.
- 공유자원을 다 사용한 후 LeaveCriticalSection을 호출하여 다른 thread의 접근을 허용.
크리티컬 섹션의 초기화
void InitializeCriticalSection ( LPCRITICAL_SECTION lpCriticalSection );
크리티컬 섹션에 진입
void EnterCriticalSection ( LPCRITICAL_SECTION lpCriticalSection );
크리티컬 섹션에 진입해제
void LeaveCriticalSection ( LPCRITICAL_SECTION lpCriticalSection );
크리티컬 섹션을 삭제
void DeleteCriticalSection ( LPCRITICAL_SECTION lpCriticalSection );
Spin Lock Count를 설정.
BOOL InitializeCriticalSectionAndSpinCount ( LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount );
* kernel-mode에 비해 빠르지만 한 프로세스 안에서만 사용이 가능하다. (mutex보다 2~10배)
대기 thread가 많을 경우, kernel-mode로 대기 thread를 관리하므로 user-mode일 때만 빠름.
* 유저영역 메모리에 존재하는 구조체이므로 한 process에 속한 thread들의 동기화만 가능하다.
* 하나의 thread에서 여러 번 호출할 때에는 이를 무시한다. (deadlock 방지를 위해)
* mutex와 달리 소유한 thread가 비정상 종료하면 다른 thread들은 무한정 대기한다.
※ critical section의 동작
1) thread가 EnterCriticalSection()으로 진입하는 경우, CRITICAL_SECTION 구조체 내부의 변수를 검사하여 off이면 on으로 변경한 후 작업을 진행. (user-mode에서 이루어짐)
2) 한 thread가 진입한 경우, single-CPU상에서는 kernel-mode로 넘어가 동기화 작업이 수행되고, multiple-CPU상에서는 Spin Count만큼 busy-waiting하며 lock해제를 대기.
3) kernel-mode로 넘어가면 해당 thread는 semaphore를 이용한 wait상태가 됨.
4) thread가 LeaveCriticalSection()으로 진입 해제하면 CRITICAL_SECTION 구조체 내부의 변수를 off로 변경하고, semaphore를 이용한 경우에는 기다리고 있는 thread에게 통지를 함.
5) EnterCriticalSection()으로 진입한 thread에서 EnterCriticalSection()을 다시 한번 호출하면, 내부적으로 count 변수 값을 올려서 deadlock을 막음. (EnterCriticalSection()을 호출한 횟수만큼 LeaveCriticalSection()을 해야 함)
※ Spin Lock
위에서처럼 critical section은 multiple-CPU상에서 critical section을 사용할 수 없는 경우,
kernel-mode의 semaphore를 사용하여 대기중인 thread를 관리하게 된다.
semaphore는 critical section을 다른 thread에서 사용 중이라면 context-switching을 하여
CPU의 소유권을 반납하게 되고 다시 자신의 차례가 되면 다시 critical section의 사용여부를
조사하고, critical section을 아직 사용 중이라면 다시 위의 동작을 반복하며 대기(busy waiting)
하게 된다.
이러한 빈번한 context-switch를 방지하기 위해 일정시간 동안 critical section의 사용이
끝나기를 기다리게 하는 것이 spin lock이다.
※ Context Switch
CPU가 다른 프로세스로 교환하기 위해 이전의 프로세스의 상태를 보관하고, 새로운 프로세스의
보관된 상태를 적재하는 작업으로 보통 process context switch가 thread context switch보다
시간이 더 소요된다.
4. EVENT
- 특정 상황이 발생했을 때 이를 알리기 위해 사용하는 kernel-mode 동기화 객체.
- mutex, semaphore, critical section은 공유자원에 대한 보호에 사용되지만, event는 주로 thread의 작업 우선순위나 작업시기의 조정을 위해 쓰임
이벤트 생성
HANDLE OpenEvent ( DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName );
이벤트 지정
BOOL SetEvent ( HANDLE hEvent );
이벤트 해제
BOOL ResetEvent ( HANDLE hEvent );
이벤트 삭제
HRESULT CloseHandle ( HANDLE hHandle );
* 윈도우 시스템에서만 존재하는 동기화 객체이다.
* 다른 객체들은 thread가 진입하게 되면 nonsignaled 상태로 자동으로 변경되게 된다.
이를 자동리셋모드 (auto-reset mode)라고 하는데 event는 자동리셋모드는 물론 수동리셋모드(manual-reset mode)도 사용할 수 있다.
=======================
=======================
=======================
스레드 프로그래밍에 대한 면접 질문시 종종 나오는 부분 입니다. 스레드 동기화 객체 크리티컬섹션, 뮤텍스, 세마포어의 차이점을 알아보자. 아래의 내용은 인터넷에 잘 퍼져있는 내용 입니다. 검색하면 바로 나오는 부분임 ㅋㅋ; 1. 크리티컬섹션 (Critical section) 유저모드 동기화 객체 커널모드 객체가 아니기 때문에 가볍고 같은 프로세스내에 스레드 동기화에 사용할 수 있다. EnterCriticalSection을 호출하면 객체는 비신호 상태가 되고, LeaveCriticalSection을 호출하면 신호상태로 바뀌어서 다른 스레드들이 접근가능하다. 2. 뮤텍스 (Mutex) 커널모드 동기화 객체 커널모드라서 크리티컬 섹션보다는 느리지만 프로세스를 넘어서 모든 스레드에 사용 될 수 있는 동기화 객체이다. 뮤텍스를 신호상태로 생성한 후 스레드에서 Wait 함수를 호출하면 뮤텍스는 비신호 상태가 되어서 다른 스레드에서는 접근하지 못한다. ReleaseMutex를 호출하면 뮤텍스는 신호상태가 되어 다른 스레드들이 접근가능하다. 3. 세마포어 (Semaphore) 커널모드 동기화 객체 뮤텍스와 비슷하지만 접근할 수 있는 스레드 갯수를 정할 수 있다. 세마포어를 생성할 때 3개의 스레드들이 접근가능하도록 지정하면 내부카운트값은 3이다. 객체 내부적으로 카운트를 관리하여 세마포어 객체를 Wait하는 스레드가 있으면 카운트가 하나씩 감소한다. 그래서 내부카운트가 0이되면 비신호상태로 바뀐다. 세마포어를 사용하고 있는 스레드들중 ReleaseSemaphore 하면 세마포어 내부카운트는 다시 1 증가하여 신호상태로 바뀌어서 다른 스레드들이 사용가능하게 된다. ※ 세마포어 생성 시 접근 가능한 스레드를 0으로 설정해서 WaitforSingleObject와 같은 효과를 내어서 사용하기도 하죠. |
=======================
=======================
=======================
출처: http://egloos.zum.com/sweeper/v/2815499
음... 멀티 쓰레드 동기화라는 좀 거대한 제목을 달았는데, 정리는 간략하게 ㅋ
그리고 임계 영역에 대한 설명과 CriticalSection 동기화 기법의 용어 구분을 확실하게 하기 위해 철저히 한글과 영문으로 분리 표기하겠음.
그리고 아래 등장하는 함수 원형과 세마포어의 카운트 값은 Windows OS 범위로 한정하여 설명한다.
1. 동기화의 두 가지 관점
쓰레드간 동기화는 크게 두 가지 관점으로 나눌 수 있다.
비록 거의 한 가지 관점의 목적을 달성하기 위한 동기화이지만...
- 실행 순서의 동기화
: 쓰레드들이 정해진 특정 순서로 실행되어야 할 때 필요하다.
: 보통 이벤트 오브젝트를 이용한 방법이 많이 사용된다. - 메모리 접근의 동기화
: 대다수 멀티 쓰레드 기반 서버 작성시 요 녀석을 해결하려고 동기화를 구현한다.
: CriticalSection, Mutex, Semaphore 등등의 다양한 방법이 있다.
: 이후 나올 내용은 모두 메모리 접근의 동기화의 관점에 필요한 내용들이다.
2. 동기화 주체로 나누어 본 두 가지 방법
쓰레드 동기화의 주체가 누구이냐에 따라 크게 두 가지 방법으로 분류할 수 있다.
- 유저 모드 동기화
: 커널의 힘을 빌리지 않는 동기화 기법이다.
: 따라서 동기화를 위해 커널로의 전환이 불필요하기에 성능상 이점은 있지만,
: 그만큼의 기능상의 제한도 있다.
: 그 종류로는 InterlockedXXX 계열 함수, CriticalSection, SpinLock - 커널 모드 동기화
: 커널에서 제공하는 동기화 기능을 활용하는 방법이다.
: 즉, 커널 오브젝트를 생성하고 이를 조작하는 함수 호출로 동기화를 진행한다.
: 커널 오브젝트 조작 함수가 호출될 때마다 커널 모드로의 전환이 발생하므로 성능 저하가 있다.
: 하지만, 유저 모드 동기화에 비해 더 강력한 기능들을 제공한다.
: 그 종류로는 Mutex, Semaphore, Event
즉, 일장일단이 있으므로 처한 상황에 맞게 골라 쓰면 될 듯.
3. 임계 영역이란?
본격적으로 메모리 접근의 동기화에 대해 살펴보기 위해 이 녀석을 얘기하지 않을 수 없다.
메모리 접근의 동기화를 해야만 하는 이유가 임계 영역을 안전하게 처리하기 위함이기 때문이다.
우선, 임계 영역의 정의부터 정리하자면,
"임계 영역이란, 배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 공유 리소스(ie. 전역변수)에 접근하는 코드 블록"
위에서 중요한 것은 특정 코드라고 표현하지 않은 것이다.
4. InterlockedXXX 계열 함수와 volatile keyword
만약 하나의 전역 변수를 동기화하는 것이 목적이라면, InterlockedXXX 계열 함수만으로도 동기화가 가능하다.
이 InterlockedXXX 계열 함수들은 Atomic Access, 즉 한 순간에 하나의 쓰레드만 접근하는 것을 보장해 주는 함수이다.
따라서 모든 쓰레드가 이 함수를 통해서 값을 변경하거나 증감시켜도 동기화 문제는 발생하지 않는다.
바로 다음에 소개할 CriticalSection이라는 동기화 기법도 내부적으로는 InterlockedXXX 함수를 기반으로 구현되어 있다.
그 다음 소개될 SpinLock 역시 이 계열 함수들을 사용하여 구현하는 경우가 많다.
InterlockedXXX 계열 함수들도 유저 모드 기반으로 동작하기에 속도 역시 상당히 빠르다.
참고로, InterlockedXXX 함수에 대한 MSDN 링크는 http://msdn.microsoft.com/en-us/library/ms684122(v=VS.85).aspx 로부터 하나씩 찾아나가면 된다.
그리고 InterlockedXXX 계열 함수들의 파라미터는 모두 volatile 키워드가 붙어 있다.
왜 volatile 키워드가 붙은 녀석들만 처리가 가능한지는 volatile 키워드의 특성을 알아야 한다.
이 설명은 다음 링크로 대체한다.
http://sweeper.egloos.com/1781856
5. CriticalSection
1) 개론
사실, 임계 영역을 영어로 표현하면 CriticalSection이다.
위에서 굳이 한글로 임계 영역이라고 표현한 이유는 이 CriticalSection 동기화 방식과의 혼동을 피하고자 함이었다.
자, 쉬운 설명을 위해서 우리가 지켜야 할 임계 영역을 화장실이라고 표현하자.
이 화장실에 들어가기 위해서는 화장실 열쇠가 필요하다.
화장실에 아무도 없다면 열쇠를 가져가 볼일을 보고, 다시 열쇠를 제자리에 가져다 두어야 한다.
그래야만 다음 사람이 다시 열쇠를 가져가 화장실에 갈 수 있기 때문이다.
여기에서 중요한 것은 반드시 열쇠를 가져간 사람이 다시 열쇠를 제자리에 가져다 놔야 한다는 것이다.
이 얘기를 굳이 하는 이유는 추후 Semaphore에서 알 수 있을 것이다.
이것이 바로 CriticalSection의 동기화 방식이다.
핵심은 "열쇠를 가진 사람만이 화장실에 들어갈 수 있다"는 것이다.
2) 사용법
CriticalSection 동기화를 사용하기 위해서는 CriticalSection 변수 선언이 필요하다.
CRITICAL_SECTION CS;
그리고 나면, 이 CS 변수를 초기화 시켜야 한다.
void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
자, 이제 열쇠를 만들고 누구나 찾아갈 수 있는 위치에 걸어둔 것까지 했다.
이제 이 열쇠를 가지고 화장실에 들락날락 하는 방법을 살펴보자.
화장실에 가기 위해 열쇠를 가져갈 때 호출해야 하는 함수는
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 이다.
만약, 누가 이미 열쇠를 가지고 화장실에 들어가 있다면, 그 누군가가 열쇠를 되돌려놓기 전까지 이 함수는 블로킹된다.
이 블로킹 되는 것이 중요한 포인트 중 하나가 되는데, 임계 영역에서의 CPU 소비 시간이 극히 짧다면, 즉 임계 영역에 머무르는 시간이 짧다면 이 블로킹 상태로 전환되는 것이 낭비가 될 수도 있다.
이는 스케쥴링 메커니즘을 이해하고 있다면 쉽게 납득이 갈텐데, 쓰레드가 블로킹 상태가 되면 쓰레드 컨텍스트 스위칭이 발생하기 때문이다.
따라서, 정말 살짝만 더 기다리면 되는데 굳이 쓰레드 컨텍스트 스위칭을 하기에, 블로킹 되는 것이 다소 낭비라는 것이다.
이 이야기를 굳이 길게 쓴 것은 바로 다음 소개할 SpinLock을 위함이며,
더 나아가 크리티컬 섹션의 스핀 기능을 추천하기 위함이다.
크리티컬 섹션의 스핀 기능을 이용할 때엔 아래와 같이 크리티컬 섹션을 초기화하면 된다.
BOOL InitializeCriticalSectionAndSpinCount( LPCRITICAL_SECTION lpCriticalSection, DWORD spinCount );
스핀 카운트는 0~0xFFFFFFFF의 범위 내 어떤 값이든 지정할 수 있으나, 대부분 4096 정도로 설정함을 추천한다.
이제 볼 일을 다 보고 열쇠를 제자리에 가져다 놓을 때 호출해야 하는 함수는
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 이다.
마지막으로 열쇠가 필요없어 졌을 때 제거하는 함수는
void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); 이다.
사실 이 CriticalSection은 가장 심플하기에 가장 보편적으로 사용되는 동기화 방식이다.
6. SpinLock
스핀락은 위 CriticalSection의 한가지 단점을 극복하는데서 착안된 동기화 기법이다.
그것은 바로 쓰레드가 임계 영역을 획득하지 못하게 되면(Lock을 못잡게 되면) 쓰레드가 블로킹되는 것이다.
쓰레드 블로킹은 이후 쓰레드 컨텍스트 스위칭을 불러오게 되며 성능의 하락을 유발시킨다.
이 단점을 극복하기 위해 스핀락은 락을 점유하지 못할 때 쓰레드가 Back-off 되어 다른 쓰레드에게 넘기는 것이 아니라, Loop를 돌면서 해당 쓰레드를 Busy-Waiting 상태로 만들어 버린다.
그러면, 쓰레드 스위칭이 발생하지 않게 되어 컨텍스트 스위칭이 발생하지 않게 되는 것이다.
하지만!!!
임계 영역에서의 작업이 다소 시간이 오래 걸리는 일이라면?
결과는 오히려 CriticalSection을 사용할 때보다 훨씬 더 안 좋아지게 된다.
곧 끝날거라 기대하며 기다리느라 쓰레드가 헛돌고 있다보면 CPU 점유율이 올라가고, 오히려 다른 쓰레드가 컨텍스트 스위칭을 하더라도 일을 더 많이 할 수 있다면 낭비가 되는 것이다.
이러한 스핀락의 단점을 해소하기 위해 아래와 같이 CriticalSection과 스핀락의 개념을 혼용하는 경우가 많다.
우선, 락을 획득하지 못하면 Busy-Waiting을 위해 스핀 카운트 만큼만 루핑한다.
그리고 스핀카운트를 다 돌았으면, Sleep()을 통해 극히 짧은 시간 쓰레드를 블로킹시킨다.
이 후 다시 쓰레드가 Running이 되면 위를 계속해서 반복하는 것이다.
이렇게 되면 무한정 BusyWaiting을 하게 되지도 않고, 굳이 쓰레드 컨텍스트 스위칭을 하지 않아도 될 경우를 많이 회피할 수 있게 된다.
물론 이 방식도 문제는 있다.
임계영역에서의 수행이 생각보다 오래 걸리는 경우엔 CriticalSection만 못한 결과가 나올 수도 있는 것이다.
따라서, 스피닝만 하는 스핀락보다는 크리티컬 섹션이 스핀락 기능을 이용하는 방식에 점점 무게가 실리고 있다.
지금까지 유저 모드 동기화 방식들에 대해 알아보았다.
앞서 얘기했듯이 커널 모드 동기화 방식들은 커널 모드로의 전환이 발생하기에 느리다.
하지만, Windows 커널 레벨에서 제공하는 동기화 기법이기 때문에, 유저 모드 동기화가 제공해 주지 못하는 기능을 제공받을 수 있다.
자, 그럼 뮤텍스와 세마포어가 어떠한 것들을 제공해주는지 어떠한 특장점이 있는지 살펴보자.
7. Mutex
CriticalSection 동기화 방식에서의 화장실 열쇠를 기억하는가?
그 열쇠가 Mutex에서는 Mutex 오브젝트이고, 이는 다음 함수를 통해 생성할 수 있다.
HANDLE CreateMutex (
LPSECURITY_ATTRIBUTES lpMutexAttributes, // Mutex도 커널 오브젝트이기에 보안 속성 지정 가능.
BOOL bInitialOwner, // Mutex를 생성한 쓰레드에게 먼저 임계 영역에 접근할 기회가 있는가?
LPCSTR lpName // Named-Mutex를 사용하려 할 때 지정해 주면 된다.
);
CriticalSection의 경우 CRITICAL_SECTION 변수를 선언하고, InitializeCriticalSection()를 통해 초기화 과정을 거쳐야 했지만, Mutex의 경우 위 함수 하나로 모든 생성/초기화 과정이 완료되었다.
위 함수의 파라미터만 봐도 CriticalSection에 비해 Mutex가 두 가지 기능이 더 많다는 것을 알 수 있다.
- bInitialOwner
: CriticalSection은 임계 영역에 먼저 접근한 녀석이면 누구나 권한이 있었지만,
: Mutex는 오브젝트를 생성한 쓰레드가 접근 권한을 먼저 취할 수 있다는 것이다. - lpName
: NULL이 아니면 Named-Mutex를 사용할 수 있게 된다.
: 보통 프로세스의 중복 실행을 방지하는데 사용된다.
::CreateMutex(0, TRUE, L"GameServer");
if ( ::GetLastError() == ERROR_ALREADY_EXISTS )
{
// 중복 실행되었으니 에러처리
}
두 번째 파라미터를 통해 커널 오브젝트의 이름을 명명할 수 있다.
위 예제에서 프로세스 중복 실행을 예로 들면서 이름을 "GameServer"라고 간단히 지었지만,
이는 Named 커널 오브젝트를 생성함에 있어 주의를 알려주기 위한 떡밥 예제이다.
커널 오브젝트의 이름은 그 종류에 관계없이 글로벌한 네임스페이스를 가진다.
즉, Mutex를 "A"로 만들고 Semaphore를 "A"로 만들려고 하면, Semaphore 생성은 실패한다.
단, 같은 Mutex라면 생성이 아닌 기존에 만들어진 커널 오브젝트를 얻어온다. (OpenMutex와 동일하게 동작)
따라서, Named 커널 오브젝트 생성시 이름을 common 한 것이나, 일반 명사 사용시에 문제점을 뒤따를 수 있다.
최대한 구체적으로 해당 오브젝트의 특징을 모두 포함시키는 것이 안전하다.
함수의 인자에 SECURITY_ATTRIBUTES가 포함되어 있는 것을 보니,
Mutex는 커널 오브젝트임이 확실하고, 커널 오브젝트는 Signaled와 NON-Signaled 상태를 가진다고 하였다.
(링크 : http://sweeper.egloos.com/2814944의 챕터 6)
커널 오브젝트는 NON-Signaled 상태에 있다가 특정 상황이 되면 Signaled 상태로 바뀌는데, 이 특정 상황이라는 것은 커널 오브젝트마다 다르다.
그렇다면, Mutex의 경우 언제 Signaled 상태로 전환될까?
"Mutex는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓인다"
그렇다면, Signaled 상태를 체크하는 WaitForSingleObject 함수를 이용하여 화장실 열쇠에 해당하는 Mutex를 획득할 수 있다.
다른 쓰레드가 잡고 있는 Mutex를 반환할 때까지(즉, Signaled 상태가 될때까지) 기다리는 것이다.
즉, CriticalSection의 EnterCriticalSection() 함수와 같은 기능을 하는 것이다.
WaitForSingleObject 함수가 Signaled를 감지하는 순간 해당 쓰레드는 Mutex를 획득한 것이고 (즉, 임계영역에 들어갈 권한을 얻은) WaitForSingleObject 함수 특성상 반환이 되는 순간 Mutex는 다시 NON-Signaled 상태가 되므로, 다른 쓰레드들은 열쇠를 얻을 때까지 기다리게 되는 셈이다.
WaitForSingleObject로 쓰레드가 대기될 때에도 쓰레드는 블로킹 상태가 되므로 역시 쓰레드 컨텍스트 스위칭은 발생한다.
WaitForSingleObject 함수명이 Mutex를 얻는 동작과 네이밍 매치가 좀 어색하다 싶으면 아래와 같이 래핑 함수를 하나 만드는 것도 나쁘지 않다.
DWORD AcquireMutex( HANDLE hMutex)
{
return ::WaitForSingleObject( hMutex, INFINITE );
}
그리고 획득한 열쇠를 반납하는 함수는
BOOL ReleaseMutex( HANDLE hMutex); 이다.
CriticalSection의 LeaveCriticalSection과 동일한 역할을 한다고 보면 된다.
마지막으로 열쇠가 필요없어졌을 때 제거하는 함수는 Mutex가 커널 오브젝트이므로,
당연히 CloseHandle 함수를 호출하면 된다.
얼핏 임계 영역에 접근을 제한하는 방법들만 보면 Mutex는 CriticalSection과 상당히 유사하다.
똑같이 Lock에 대한 소유자(Owner)가 존재하고, 소유자만이 락을 해제할 수 있다.
하지만, Mutex는 위에서 정리한 대로 CriticalSection이 갖지 못하는 부가 기능이 있으며 유저 모드 동기화 vs 커널 모드 동기화의 차이점도 존재한다.
또한, Name 명명을 이용하여 프로세스간 동기화 역시 가능하다.
(아래에 나올 Semaphore나 Event 역시 이름을 명명할 수 있는 커널 오브젝트이다)
A 프로세스에서 먼저 "AAA" named Mutex를 이용해 임계 영역에 들어가면,
B 프로세스에서 "AAA" named Mutex가 signaled 상태가 될 때까지 기다리게 할 수 있는 것이다.
8. Semaphore
보통 세마포어는 뮤텍스와 상당이 유사하다고들 얘기한다.
세마포어는 그 종류가 몇가지 되는데 그 중 하나가 뮤텍스이다.
즉, 뮤텍스는 엄밀히 얘기해 세마포어의 여러 종류 중 하나인 셈이다.
따라서, 세마포어 역시 커널 오브젝트 형태이고 나머지 처리들이 뮤텍스와 유사함을 알 수 있다.
(WaitForSingleObject를 통한 접근 대기라던가...)
그럼 세마포어는 무엇이 다르길래 뮤텍스와 별도로 존재하는 것일까?
그 차이는 바로 Count 기능이다. Count 기능?
쉽게 설명하기엔 역시 예를 들어서 설명하는 것이 최고다.
CriticalSection과 Mutex를 설명할 때엔 한번에 한명만 들어갈 수 있는 화장실과 그 열쇠에 비유했지만,
세마포어는 카운트 기능을 설명하기 위해 식당(임계 영역)과 테이블(접근 키)로 비유해보자.
유명한 식당에 테이블은 딱 10개뿐인데, 손님은 50명이 대기중이다. (한 테이블엔 한명만 앉을 수 있다고 가정)
즉, 생성된 쓰레드 수는 50개고 임계 영역에 동시 접근할 수 있는 쓰레드는 10개가 되는 셈이다.
뮤텍스는 임계 영역에 접근가능한 쓰레드 개수를 조절하는 기능이 없고, 세마포어엔 있다.
이것이 바로 카운트 기능인 것이다. 이게 뮤텍스와 세마포어의 결정적인 차이다.
다음은 세마포어를 생성하는 함수이다.
HANDLE CreateSemaphore (
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 세마포어도 커널 오브젝트이기에 보안 속성 지정 가능.
LONG InitialCount. // 생성시 세마포어의 카운트 값
LONG MaximumCount, // 세마포어가 가질 수 있는 최대 카운트 값. InitialCount보단 당연히 커야 함
LPCSTR lpName // Mutex와 동일하게 Named 지정 가능
);
세마포어도 커널 오브젝트이기에 Signaled, NON-Signaled 상태가 전환된다.
"세마포어는 카운트가 0인 경우 NON-Signaled 상태가 되고, 1 이상인 경우 Signaled 상태가 된다"
초기 카운트 값인 InitialCount를 10으로 잡았다면, 세마포어는 Signaled 상태이고 이 상황에서
WaitForSingleObject가 한번씩 반환될 때마다 카운트는 줄어들어 0이 되는 순간 NON-Signaled 상태가 되는 것이다.
그 이후 접근한 쓰레드들은 다른 쓰레드가 임계 영역에서 벗어나 세마포어를 반환할 때까지 블로킹 된다.
참고로, InitialCount가 1인 세마포어는 Binary Semaphore라 불리며 기능적으로는 Mutex와 동일하다.
임계 영역을 벗어난 쓰레드가 세마포어를 반환할 때 사용하는 함수는
BOOL ReleaseSemaphore (
HANDLE hSemaphore, // 세마포어 오브젝트 핸들
LONG ReleaseCount, // 반환할 세마포어 카운트. 상황에 따라서 1보다 크게 줄수도 있다.
LPLONG PreviousCount // 이전 카운트 값인데 불필요하면 NULL 때리삼
);
위 함수 호출시 ReleaseCount는 결국 세마포어 카운트 증가치가 되는데, 이 증가치와 기존 값의 합이 MaximumCount를 넘게 되면, ReleaseSemaphore는 카운트를 변경시키지 못하고 FALSE를 리턴한다.
그리고 다 쓴 세마포어 제거 역시 CloseHandle 함수를 통하면 된다.
마지막으로 CriticalSection/Mutex와 세마포어의 중요한 차이점을 설명하겠다.
이것은 아주 중요한 개념인데, 바로 Owner 개념의 유무이다.
CS/Mutex는 획득한 쓰레드(Owner)가 직접 반환하는 것이 원칙이다. 획득 쓰레드가 아니면 반환할 수 없다.
하지만, 세마포어의 경우 획득한 쓰레드와 반환 쓰레드가 달라도 문제가 되지 않는다.
즉, Owner 개념이 없는 것이다.
다시 말해 CS/Mutex의 경우 획득 쓰레드가 비정상 종료되어 버리면, 반환 문제가 발생한다.
- CriticalSection
: 이건 뭐 답이 없다.
: Lock을 획득한 쓰레드를 비정상 종료 시키면 다른 쓰레드는 영원히 임계 영역에 접근하지 못한다.
: 하지만, 다른 쓰레드가 EnterCritialSection를 호출했다고 해서 의해 영~원~히~ 블로킹 되지는 않는다.
: 시스템에 CriticalSectionTimeout 값이 지정되어 있으며, 이 값이 초과할 때까지 반환이 이뤄지지 않으면 예외가 발생한다.
: 이 값은 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager 에 위치한다.
: 기본값은 2,592,000이며 이는 대략 30일에 해당한다.
: 즉, 임계영역은 블로킹이 지속되나, 임계영역에 진입을 시도한 쓰레드의 타임아웃은 너~무~ 길지만 영원하진 않다는 거다. - Mutex
: 역시 커널 동기화 기법인가보다.
: 커널에서 쓰레드와 Mutex 오브젝트를 지속적으로 감시하고 있는 듯~
: 획득 쓰레드가 비정상 종료되는 순간, 다른 쓰레드의 WaitForSingleObject 함수가 바로 리턴된다.
: 리턴값은 0x80 이며 이는 WAIT_ABANDONED에 해당한다.
: 즉, Mutex의 경우 Owner가 반환하는 것이 원칙이나, Owner가 죽어버렸을 경우 커널이 대신 반환해 주는 것이다.
=======================
=======================
=======================
크리티컬 섹션(Critical Section)
내부적으로 Interlock을 사용하므로 쓰레드 동기화 중에서 가장 빠른 속도로 동작한다.
뮤텍스와 다른 점은 단일 프로세서에 한해서만 동작이 가능하다는 것.
사용 방법은 아래와 같다.
* WIN API
Critical Section 초기화
void WINAPI InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Critical Section 릴리즈
void WINAPI DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Lock 설정
void WINAPI EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Lock 해제
void WINAPI LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
Example)
CRITICAL_SECTION m_cs;
InitializeCriticalSection(&m_cs);
EnterCriticalSection(&m_cs);
동기화가 필요한 코드....
LeaveCriticalSection(&m_cs);
EnterCriticalSection(&m_cs);
동기화가 필요한 코드....
LeaveCriticalSection(&m_cs);
DeleteCriticalSection(&m_cs);
* MFC
CCriticalSection g_CS;
g_CS.Lock();
동기화가 필요한 코드...
g_CS.Unlock();
출처: http://naito.tistory.com/entry/크리티컬-섹션Critical-Section [우진샘 블로그]
=======================
=======================
=======================
UI Thread
Worker Thread와의 차이는 저번에 알아본 바와 같이 Windows Message 펌프가 있는가 없는가의 차이가 있을 뿐이다.
결국 UI쓰래드를 하나 생성한다면 윈도우 메시지처리를 하는 또 하나의 쓰래드를 만드는 것이다.
UI 쓰래드를 만드는 방법은 MFC를 사용하는 방법이 유일한 것 같다. (일반 Win32 API로는 UI쓰래드를 만들기가 힘든 것 같
다.) 그 방법은 CWinThread 클래스로부터 UI메시지 처리를 하게될 새로운 객체를 파생시키기만 하면 된다. 그 후
AfxBeginThread를 사용하거나 CWinThread 클래스의 CreateThread 멤버 함수를 사용해서 쓰래드를 시작하면 된다.
UI 쓰래드는 성격상 UI쓰래드가 받아온 메시지를 처리할 또다른 윈도우가 필요하다(그 결과를 윈도우에 표시할 필요가 없다면
꼭 필요한 것은 아니다.) 반복되는 얘기겠지만 UI쓰래드가 실행되면 현재의 시스템에서 발생되는 모든 윈도우 메시지들은 그
쓰래드에게 적절하게 자동으로 보내지게된다.
MSVC의 온라인 헬프에서 "multithreaded programming" 이란 항목으로 조사를 하다보면 몇 가지 샘플 프로그램들이 있는데,
거기에서 UI 쓰래드 예제는 단 한가지 밖에 없다. "MTMDI" 라는 것인데, MDI 프로젝트에서 각 View 윈도우마다 각자의
UI쓰래드를 두어 사용하는 것이었다. 그것이 UI쓰래드의 가장 적절한 사용처가 아닐까 싶다.
어떤 경우가 UI 쓰래드를 위한 가장 좋은 예가 될까... 나도 잘 모르겠다... 혹시 아시는 분 있으면 연락 바란다.
Critical Section(임계 섹션)
이전 스터디에서 잠깐 언급했던 글로벌 변수를 이용한 쓰래드간의 정보교환에 의한 버그를 신경쓰지 않고 쓰래드들 간에
글로벌 데이터를 공유하고자할 때 유용하게 쓰이는 방법이 바로 Critical Section을 사용하는 것이다.
* 이벤트는 시그널화에, 임계 섹션은 데이터에 대한 억세스를 제어하는데 적합하다.
만약 쓰래드 A와 B가 시간(Time)값을 공유한다고 가정해 보자, A가 시간(Hour)값을 변경하고 분(Minute)값을 변경하려는
시점에 쓰래드 B가 시간값을 참조하게 된다면(인터럽트) 쓰래드 B가 참조하는 시간(Time)값은 전혀 엉뚱한 것이 될 것이다.
그럼, 다음의 클래스를 살펴보자.
float4 fvTotalAmbient = fvAmbient * fvBaseColor;
float4 fvTotalDiffuse = fvDiffuse * fNDotL * fvBaseColor;
return saturate( fvTotalAmbient +a#include "stdafx.h"
class CHMS
{
private:
int m_nHr, m_nMn, m_nSc;
CRITICAL_SECTION m_cx;
Public:
CHMS() : m_nHr(0), m_nMn(0), m_nSc(0)
{
::InitializeCriticalSection(&m_cs);
}
~CHMS()
{
::DeleteCriticalSection(&m_cs);
}
void SetTime(int nSecs)
{
::EnterCriticalSection(&m_cs);
m_nSc = nSecs % 60;
m_nMn = (nSecs/60) % 60;
m_nHr = nSecs / 3600;
::LeaveCriticalSection(&m_cs);
}
int GetTotalSecs()
{
int nTotalSecs;
::EnterCriticalSection(&m_cs);
nTotalSecs = m_Hr*3600 + m_nMn*60 + m_nSc;
::LeaveCriticalSection(&m_cs);
return nTotalSecs;
}
void IncrementSecs()
{
::EnterCriticalSection(&m_cs);
SetTime( GetTotalSecs() + 1 );
::LeaveCriticalSection(&m_cs);
}
}; fvTotalDiffuse );
위의 클래스는 시간값(시,분,초)을 참조/수정 하기위한 데이터와 함수들을 가지고 있다. 이 클래스의 멤버중에
CRITICAL_SECTION 타입의 멤버가 있음을 주목하자. 생성자는 InitializeCriticalSection 함수를 호출하며 소멸자는
DeleteCriticalSection 함수를 호출한다. 그리고 다른 함수들은 EnterCriticalSection 과 LeaveCriticalSection 함수를 호출한다.
*위의 예제는 아마도 역시 MSDN의 부실한 헬프에서 따다온 것일텐데, 그래도 Criticla Section의 다양한 면을 보는데 아주
좋은것 같다.
즉, 위의 코드를 간단히 설명하자면, 어떤 지정된 출입문(Critical section이 바로 그 문이다) 을 통과해야만 어떤 물건
(여기서는 시간값이 되겠지...)를 만질 수 있다는 것이다. 이 문은 한번에 한 사람만 들어갔다 나올 수 있다.
어랏, 근데 위의 코드를 보면 IncrementSecs 함수가 SetTime를 호출하는데, 이 두 함수 모두 하나의 임계섹션을 사용하게
된다. 그렇게 되면 문제가 생기지 않을까? 신기하게도 문제가 생기지 않는다...
*이렇게 critical section이 '중첩'되면 Windows가 이 중첩수준을 추적해 줄 것이다.
*힙에서 생성한 객체 포인터들을 공유할 경우는 또 다른 문제들이 발생한다. 각 쓰래드는 다른 쓰래드가 객체를 삭제했는가의
여부를 판단해야만 하고(당연하지요) 그렇게되면 여러분이 억세스를 포인터들의 삭제/생성 시점과 동기화를 시켜줘야만 한다.
만약 MFC 클래스들을 사용한다면 더 프로그램 짜기가 쉬워질 것이다. 나는 CCriticalSection 객체를 하나 만들어 동기화
준비를 해 놓고 CSingleLock 객체를 사용하여 CCriticalSection 객체를 Lock, Unlock 하면서 쉽게 동기화를 구현한다..
자세한 설명은 MSDN 헬프를 보시길..
MUTEX
Mutex란? (Mutual Exclusion<상호 배타>의 준말) 는 상호간에 asynchronously 하게 작동하는 쓰래드간 또는 프로세스간에
'통신'을 위한 한 방법이다. 이 '통신'은 주로 다수의 쓰래드(또는 프로세스)들의 공유 리소스에 대한 접근을 리소스를
"locking" 과 "unlocking"을 통해 조율하게된다.
특정 x,y 좌표에 점을 찍는 프로그램을 만들 때, A쓰래드가 x,y 좌표를 변경하는 일을 하고 B쓰래드는 그 변경된 좌표값 대로
화면을 점을 찍는 역할을 한다면, 이경우 역시 두 쓰래드간에 동기화가 되어야 정확한 좌표에 점을 찍을 수 있게된다. 그래서
쓰래드 A가 좌표값을 변경하는 경우는 다른 어떠한 쓰래드도 그 값을 참조/변경하지 못하도록 막아놓고(Lock: mutex를
set한다.) 그 작업이 끝나면 잠금을 풀어주어(Unlock: mutex를 clear한다.) 쓰래드 B가 그 좌표값을 읽어와서 화면에 점을
찍을 수 있도록 하는 것이 정석이다. Mutex가 설정되어 있는 경우 다른 쓰래드는 Mutex가 해제될 때까지 기다려야만 한다.
* 그럼 Critical Section과의 차이는 무엇인가? 그 첫째는 Critical Section은 좀더 빠른 처리를 요하는 경우, 그리고 그 공유
하고자 하는 리소스가 프로세스와 프로세스사이는 넘을 수 없는 경우(단지 쓰래드 사이에서만 공유하는 경우)에 유용하다.
CriticalSection과의 차이점 그 두번째는? 으하하 사용방법의 차이이다…. (별거아니군) 화면에 점을 찍을 준비가 된 쓰래드는
WaitForSingleObject를 호출하여 Mutex이 해제될 때까지 기다렸다가 mutex을 설정하고 점을 찍게 된다. 점을 다 찍고 나면
다시 mutex를 해제시켜준다. 다음의 예제소스를 참고하길 바란다.
좀더 복잡한 경우를 생각해보자. 만약 어떤 프로그램에서 복수의 쓰래드가 동일한 파일을 사용하는 경우, 다른 쓰래드가 파일
포인터를 엉뚱한 곳으로 이동시켜버렸을 수 있으므로, 각 쓰래드는 읽거나 쓰기 전에 반드시 파일포인터를 재설정 해야만
한다. 추가로, 각 쓰래드는 파일 포인터가 자신이 파일 포인터를 소유하고 파일을 접근하는 사이에 이미 다른 쓰래드에 의해
선점되지 않았는지 확인해야만 한다.
HANDLE hIOMutex= CreateMutex (NULL, FALSE, NULL);
WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);
다음의 예제를 보도록 하자. MSDN에서 따온 예제이다... 좀 길지만 차근차근 보다 보면 소스코드가 이해가 갈 것이다.
백번 설명하는 것 보단 한번 소스코드를 이해하는 것이 더 낫다.
/* Bounce - Creates a new thread each time the letter 'a' is typed.
* Each thread bounces a happy face of a different color around the screen.
* All threads are terminated when the letter 'Q' is entered.
*
* This program requires the multithread library. For example, compile
* with the following command line:
* CL /MT BOUNCE.C
*/
#include
#include
#include
#include
#include
#include
#define MAX_THREADS 32
/* getrandom returns a random number between min and max, which must be in
* integer range.
*/
#define getrandom( min, max ) ((rand() % (int)(((max) + 1) - (min))) + (min))
void main( void ); /* Thread 1: main */
void KbdFunc( void ); /* Keyboard input, thread dispatch */
void BounceProc( char * MyID ); /* Threads 2 to n: display */
void ClearScreen( void ); /* Screen clear */
void ShutDown( void ); /* Program shutdown */
void WriteTitle( int ThreadNum ); /* Display title bar information */
HANDLE hConsoleOut; /* Handle to the console */
HANDLE hRunMutex; /* "Keep Running" mutex */
HANDLE hScreenMutex; /* "Screen update" mutex */
int ThreadNr; /* Number of threads started */
CONSOLE_SCREEN_BUFFER_INFO csbiInfo; /* Console information */
void main() /* Thread One */
{
/* Get display screen information & clear the screen.*/
hConsoleOut = GetStdHandle( STD_OUTPUT_HANDLE );
GetConsoleScreenBufferInfo( hConsoleOut, &csbiInfo );
ClearScreen();
WriteTitle( 0 );
/* Create the mutexes and reset thread count. */
hScreenMutex = CreateMutex( NULL, FALSE, NULL ); /* Cleared */
hRunMutex = CreateMutex( NULL, TRUE, NULL ); /* Set */
ThreadNr = 0;
/* Start waiting for keyboard input to dispatch threads or exit. */
KbdFunc();
/* All threads done. Clean up handles. */
CloseHandle( hScreenMutex );
CloseHandle( hRunMutex );
CloseHandle( hConsoleOut );
}
void ShutDown( void ) /* Shut down threads */
{
while ( ThreadNr > 0 )
{
/* Tell thread to die and record its death. */
ReleaseMutex( hRunMutex );
ThreadNr--;
}
/* Clean up display when done */
WaitForSingleObject( hScreenMutex, INFINITE );
ClearScreen();
}
void KbdFunc( void ) /* Dispatch and count threads. */
{
int KeyInfo;
do
{
KeyInfo = _getch();
if( tolower( KeyInfo ) == 'a' && ThreadNr < MAX_THREADS )
{
ThreadNr++;
_beginthread( BounceProc, 0, &ThreadNr );
WriteTitle( ThreadNr );
}
} while( tolower( KeyInfo ) != 'q' );
ShutDown();
}
void BounceProc( char *MyID )
{
char MyCell, OldCell;
WORD MyAttrib, OldAttrib;
char BlankCell = 0x20;
COORD Coords, Delta;
COORD Old = {0,0};
DWORD Dummy;
/* Generate update increments and initial display coordinates. */
srand( (unsigned) *MyID * 3 );
Coords.X = getrandom( 0, csbiInfo.dwSize.X - 1 );
Coords.Y = getrandom( 0, csbiInfo.dwSize.Y - 1 );
Delta.X = getrandom( -3, 3 );
Delta.Y = getrandom( -3, 3 );
/* Set up "happy face" & generate color attribute from thread number.*/
if( *MyID > 16)
MyCell = 0x01; /* outline face */
else
MyCell = 0x02; /* solid face */
MyAttrib = *MyID & 0x0F; /* force black background */
do
{
/* Wait for display to be available, then lock it. */
WaitForSingleObject( hScreenMutex, INFINITE );
/* If we still occupy the old screen position, blank it out. */
ReadConsoleOutputCharacter( hConsoleOut, &OldCell, 1, Old, &Dummy );
ReadConsoleOutputAttribute( hConsoleOut, &OldAttrib, 1, Old, &Dummy );
if (( OldCell == MyCell ) && (OldAttrib == MyAttrib))
WriteConsoleOutputCharacter( hConsoleOut, &BlankCell, 1, Old, &Dummy );
/* Draw new face, then clear screen lock */
WriteConsoleOutputCharacter( hConsoleOut, &MyCell, 1, Coords, &Dummy );
WriteConsoleOutputAttribute( hConsoleOut, &MyAttrib, 1, Coords, &Dummy );
ReleaseMutex( hScreenMutex );
/* Increment the coordinates for next placement of the block. */
Old.X = Coords.X;
Old.Y = Coords.Y;
Coords.X += Delta.X;
Coords.Y += Delta.Y;
/* If we are about to go off the screen, reverse direction */
if( Coords.X < 0 || Coords.X >= csbiInfo.dwSize.X )
{
Delta.X = -Delta.X;
Beep( 400, 50 );
}
if( Coords.Y < 0 || Coords.Y > csbiInfo.dwSize.Y )
{
Delta.Y = -Delta.Y;
Beep( 600, 50 );
}
}
/* Repeat while RunMutex is still taken. */
while ( WaitForSingleObject( hRunMutex, 75L ) == WAIT_TIMEOUT );
}
void WriteTitle( int ThreadNum )
{
char NThreadMsg[80];
sprintf( NThreadMsg, "Threads running: %02d. Press 'A' to start a thread,'Q' to quit.", ThreadNum );
SetConsoleTitle( NThreadMsg );
}
void ClearScreen( void )
{
DWORD dummy;
COORD Home = { 0, 0 };
FillConsoleOutputCharacter( hConsoleOut, ' ', csbiInfo.dwSize.X * csbiInfo.dwSize.Y, Home, &dummy );
}
Semaphore
세마포어는 한정된 수의 사용자만을 지원할 수 있는 공유 자원에 대한 접근을 통제하는데 유용하다. MFC에서는 지금까지
알아본 Critical Section, Mutex, Semaphore에 대한 클래스를 제공해 준다. 이 클래스들은 모두 CSyncObject에서 파생되었
으며, 따라서 사용법도 비슷하다.
다음의 내용은 어느 경우에 어떤 방식을 사용할 것인지에 대한 것으로, MS VC++ 5.0의 Online help에
"Multithreading: When to Use the Synchronization Classes" 라는 제목으로 개제되어 있는 내용중의 일부이다.
To determine which synchronization class you should use, ask the following series of questions:
1. 프로그램이 자원에 접근하기 전에 어떤 일이 발생할 때까지 기다려야만 합니까?
그렇다면 CEvent를 사용하십시오.
2. 동일한 프로그램내에 존재하는 하나 이상의 쓰래드가 동시에 같은 리소스를 접근해야 합니까? (예를 들어 하나의 다큐먼트에
대해 다섯개의 뷰를 제공하려고 할 때)
그렇다면 CSemaphore.를 사용하십시오.
3. 하나 이상의 프로그램이 특정 리소스를 사용하도록 하겠습니까?(예를 들어 DLL에 들어 있는 리소스)
그렇다면 CMutex.를 사용하시고
아니라면 CCriticalSection. 을 사용하십시오.
출처: http://adolys.tistory.com/entry/본문스크랩-Critical-Section-MUTEX-Semaphore-에-대하여 []
=======================
=======================
=======================
출처: http://genesis8.tistory.com/154
크리티컬 섹션
운영체제가 지원하는 동기화 방법의 하나로 크리티컬 섹션(Critical Section)은 번역하자면 '임계 구역' 혹은 '치명적 영역' 이라고 할 수 있다. 치명적 영역은 보호되어야하듯이, 이 역시 보호되어야 할 영역을 이른다.'공유 자원의 독점을 보장해주는 역할을 수행한다.
스레드 동기화시 커널의 기능을 사용하지 않고 동기화하는 유저 모드 동기화와, 커널로 전환한 후 커널 모드로 행하는 동기화(유저 모드보다 많은 기능)로 나뉘는데,. 크리티컬 섹션은 커널 오브젝트를 사용하지 않고 동기화하는 방법이다.
특징
커널 객체를 사용하지 않는다.
서로 다른 프로세스간에 접근이 불가하다.
내부적으로 인터락 함수를 사용하고 있다.
커널 오브젝트를사용하지 않기 때문에 핸들을 사용하지 않고, CRITICAL_SECTION 이라는 타입을 정의하여 사용한다.
비교
크리티컬 섹션은 사용자 객체를 사용. 커널 객체가 아니므로 가볍고 빠르다. 그러나 한 프로세스 내의 쓰레드 사이에서만 동기화가 가능. 보통의 경우 가볍고 쉽게 쓸 수 있는 동기화 객체.
뮤텍스는 커널 객체를 사용. 크리티컬 섹션보다 무겁다. 크리티컬 섹션이 한 프로세스 내의 쓰레드 사이에서만 동기화가 가능한 반면, 뮤텍스는 여러 프로세스의 스레드 사이에서 동기화가 가능하다. 뮤텍스를 가장 흔히 사용하는 예가 프로세스 다중 실행을 막을 때. 이런 기능은 크리티컬 섹션으로는 불가능.
3. 세마포어 역시 커널 객체. 크리티컬 섹션, 뮤텍스는 동기화 함에 있어서 동시에 하나의 쓰레드만 실행되게 하는데 이에 반해, 세마포어는 지정된 수만큼의 쓰레드가 동시에 실행되도록 동기화하는 것이 가능합니다. 지정된 수보다 작거나, 같을 때까지 쓰레드의 실행을 허용하고, 지정된 수를 넘어서 쓰레드가 실행되려 하면 실행을 막는다.
원리
현재 수행 중인 스레드가 크리티컬 섹션을 벗어나기 전까지는 동일 리소스에 접근하려고 하는 다른 리소스를 스케쥴 하지 않게 한다.
EnterCriticalSection는 처음에 들어온 것은 접근 권한을 주지만, 리소스를 선점 중인 다른 대상이 있다면. 그 이후의 접근은 대기 시킨다. (스레드가 대기 상태로 변하므로, CPU의 시간을 낭비하지 않게 된다.)
LeaveCriticalSection은 CRITICAL_SECTION을 갱신한다.
사용
다음 두 함수로 초기화 및 파괴한다.
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
이것은 크리티컬 섹션을 사용하기 위해서 갖춰야할 준비라고 할 수 있고, (뒷 정리)
실제로 크리티컬 섹션을 적용할 때는 다음의 함수를 사용한다.
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
이 두 함수 사이의 코드가 크리티컬 섹션이 된다.
EnterCriticalSection(&crit);
// 코드를 삽입한다.
LeaveCriticalSection(&crit);
※주의
만일 LeaveCriticalSection을 사용하지 않을 경우, 대기중인 스레드는 작업이 끝나지 않았다고 보고 계속해서 대기 하게 된다.
크리티컬 섹션 영역에 의해서 X를 가지지 못한 쓰레드는 Enter함수에 의해 대기 당하므로 크리티컬 섹션에 들어가지 못한다. 그러고보니 크리티컬 섹션은 두 곳 모두가 다른 코드인데도 공유되고 있다. 크리티컬 섹션은 일종의 '키' 에 비유되는데, 요즘으로 치면 집에 들어가고 나오기 위한 카드와 같다. 가지고 있을 때는 입장할 수 있지만, 그렇지 않을 때는 해당 영역을 들어가지 못하니 말이다. (안에서 열어주는 건 예외..)
개 수에 상관 없이 보호될 영역을 Enter ~ Leave로 감싸면 공통적으로 보호 상태가 되는 듯 하다.
크리티컬 섹션을 통해서 작업을 대기 중인 스레드는 CPU 시간을 포기하게 하고, 스위칭이 발생하지 않는다.
크리티컬 섹션을 사용할 때, 예외가 발생하여 Leave가 호출되지 못하는 등의 사고가 일어날 수됴 있다.
try
{
EnterCriticalSection(&crit);
...
}
finally
{
LeaveCriticalSection(&crit);
}
따라서 이와 같은 코딩이 예외에 대처할 수 있는 방법이 된다. finally의 특성상 실행한 코드가 폭파되건 도중에 반환이 되건 상관 없이 절대적으로 실행되기 때문에, 혹시나 일어날 수 있는 불상사에 대비할 수 있는 방법이 된다.
참고 출처
http://seedyoon.tistory.com/entry/Critical-Section
http://egloos.zum.com/CharlesM/v/1017131
출처: http://genesis8.tistory.com/154 [프로그래머의 블로그]
=======================
=======================
=======================
출처: http://egloos.zum.com/byung/v/4375754
일반적으로 Critical Section은 둘 이상의 Threads와 관련하여 공유된 resource 에 대한 동기화를 위해 가장 흔히 사용하는 오브젝트이다. 그리고, 다음은 msdn 문서에서 보여 주고 있는 사용 예이다.
// Global variable
CRITICAL_SECTION CriticalSection;
void main()
{
...
// Initialize the critical section one time only.
if (!InitializeCriticalSectionAndSpinCount(&CriticalSection,
0x80000400) )
return;
...
// Release resources used by the critical section object.
DeleteCriticalSection(&CriticalSection)
}
DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
...
// Request ownership of the critical section.
EnterCriticalSection(&CriticalSection);
// Access the shared resource.
// Release ownership of the critical section.
LeaveCriticalSection(&CriticalSection);
...
}
Critical Section은 Mutex와 다르게 User Mode Object로 handle이나 name을 갖고 있지 않다. 그러므로, 다른 processes의 threads간 동기화를 할 수 없으나, performance 측면에서는 가장 효과적인 동기화 Object로 많이 사용되고 있다. 이러한 Critical Section에 의한 locking 현상은 debugging 시에 !lock command를 이용하여 확인할 수 있다. 아래를 확인해 보면, lock resource가 무엇인지, lock count의 정보 뿐만 아니라, own하고 있는 thread의 정보를 확인할 수 있다.
0:000> !locks
CritSec ntdll!LoaderLock+0 at 77FCF348
LockCount 6
RecursionCount 1
OwningThread a08
EntryCount 2c1
ContentionCount 2c1
*** Locked
0:000> ~~[a08]s
eax=00000003 ebx=00000000 ecx=00000010 edx=00000000 esi=00a8122c edi=00000000
eip=77f827e8 esp=0430f8b0 ebp=0430f920 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
ntdll!ZwWaitForSingleObject+0xb:
77f827e8 c20c00 ret 0Ch
0:038> kb
ChildEBP RetAddr Args to Child
0430f8ac 77f83955 00000acc 00000000 00000000 ntdll!ZwWaitForSingleObject+0xb
0430f920 77f838c6 00a81200 1f7f212a 00a8122c ntdll!RtlpWaitForCriticalSection+0x9e
0430f928 1f7f212a 00a8122c 00ab78f0 1f7f816a ntdll!RtlEnterCriticalSection+0x46
0430f934 1f7f816a 1f82120c 1b66fa04 1b66f9e0 ODBC32!MPEnterCriticalSection+0x11
!lock command는 Owing Thread에 대한 정보를 보여주기 때문에 현재 누가 Critical Section에 들어 갔는지 확인 할 수 있으므로, 해당 Thread의 call stack을 확인함으로써 Lock 현상이 Deadlock situation 을 가져오는 지 여부를 verify해볼 수 있다.
또한, 간혹, 아래와 같이 lock count가 증가했지만, Owing Thread는 0이거나 알 수 없는 경우가 있는 데,
0:038> !critsec 69611878
CritSec at 69611878
LockCount 48
RecursionCount 1
OwningThread 0
. . .
이러한 경우는 Critical Section에 들어간 Thread내부 operation에서 뭔가 Exception이나 문제가 발생하여 Critical Section Lock을 release하지 못한 상태에서 소멸된 것이 아닌 가 추측할 수 있다. 이것이 Critical Section의 가장 큰 문제인데, Mutex의 경우는 동기화 개체를 소유한 상태에서 해당 Thread에 문제가 발생하여 drop 되는 경우에 thread가 terminated되는 시점에 signal이 sending 됨으로써 그런 문제가 발생하지 않지만, Critical Section의 경우는 해당 locking된 resource의 owing thread가 resource 를 own 한 상태에서 소멸되더라도 lock이 풀리지 않기 때문에 다른 어떤 threads도 그 이후로 다시는 사용하지 못하는 결과를 가져오게 된다. 그러므로, Critical Section 을 사용할 때, Try Catch 문을 사용하여 혹, Exception이 발생할 때, handling 하도록 하고 특히 finally 문에서 LeaveCriticalSection 을 호출하도록 하여 Exception이 발생하더라도 적어도 Critical Section을 놓을 수 있도록 하는 것이 필요하다.
=======================
=======================
=======================
출처: http://kuaaan.tistory.com/99
Q1: 동일한 스레드에서 동일한 CriticalSection에 두번 진입하면 Block이 걸릴까?
A1: 아니다. 스레드가 이미 자신이 소유한 CriticalSection을 다시 소유하려고 시도하는 것은 전혀 문제될 것이 없다. CriticalSection 개체는 내부적으로 LockCount와 OwningThread라는 멤버가 있어서 자신이 몇번 Lock이 걸렸는지와 어느 스레드에 소유되었는지를 기억하고 있다.
다른 스레드가 소유한 CriticalSection 개체에 대해 EnterCriticalSection을 시도하면 당연히 Block되겠지만, CriticalSection을 소유한 스레드가 다시 EnterCriticalSection을 시도하면 즉시 리턴되고, 내부적으로 LockCount가 하나 증가한다. 대신, EnterCriticalSection한 횟수만큼 LeaveCriticalSection을 호출해주어야 CriticalSection 개체가 Signaled 상태로 돌아온다. 두번 EnterCriticalSection한 후 한번만 LeaveCriticalSection하면? 당연히 한번 EnterCriticalSection했을 때의 상태와 동일해진다.
디버거에서 CriticalSection 개체를 들여다 보면 아래와 같은 멤버들로 구성된 것을 알 수 있다. LockCount의 초기값은 -1이고, 한번 소유되면 0으로 증가한다.
※ 참고로... linux의 posix mutex는 lock을 두번 걸 경우 데드락에 걸리는 문제가 있었다.
Q2: A라는 스레드가 소유한 CriticalSection을 B라는 제3의 스레드가 해제할 수 있나?
A2: 그렇다. CriticalSection에 진입할 때는 Thread를 확인하지만 Leave할때는 확인하지 않으며, 임의의 스레드라도 CriticalSection을 해제할 수 있다.
Q3: 어느 Thread에도 소유되지 않은 CriticalSection에 대해 LeaveCriticalSection을 호출하면 어떻게 되나?
A3: 이런 일이 벌어져선 안된다 ㅡ.ㅡ. LeaveCriticalSection을 호출하면 LockCount가 초기값인 -1에서 하나 더 감소하여 -2가 된다. 이 상태에서는 LeaveCriticalSection을 호출했던 스레드를 포함하여 어떠한 스레드도 EnterCriticalSection을 할 수 없게 된다. (시도하면 Block된다.)
이런 경우 CriticalSection을 삭제하고 다시 만드는 방법밖에 없다.
Q4: CriticalSection을 소유한 Thread가 죽어버리면 CriticalSection은 Lock이 풀릴까?
A4: 풀리지 않는다. (반면에 커널객체인 Mutex나 Event 등은 소유한 Process가 죽으면 소멸된다.) 따라서 이런 일이 벌어지면 Application이 Hang 걸려버리는 경우가 생긴다. 단, 위에서 언급한 바와 같이 소유한 Thread가 죽어버린 CriticalSection을 제 3의 Thread가 LeaveCriticalSection 해줄 수는 있다.
※ Mutex의 경우 해당 개체를 소유한 Thread가 죽으면 자동으로 Signaled 상태로 변경된다. 즉, 저절로 Lock이 풀리게 된다. 이때 WaitForSingleObject()로 대기중이던 스레드는 함수 리턴값으로 WAIT_ABANDONDED 을 받게 된다. (CriticalSectioin과 Mutex의 차이)
출처: http://kuaaan.tistory.com/99 [달토끼 대박나라~!! ^^]
=======================
=======================
=======================
Critical Section (크리티컬 섹션) 을 설정한 구간은 한 번에 하나의 "스레드"에서만 사용가능하다. - 프로세스 내에 여러 스레드가 있는 환경에서, 우리가 설정해둔 "크리티컬 영역" 에 어떤 스레드가 먼저 진입하여 크리티컬 영역을 벗어나지 않은 상태에서는 동일 프로세스의 다른 스레드에서 해당 크리티컬 영역에 진입하는 것을 금지한다. - 후발 스레드의 "크리티컬 섹션" 진입금지 방식에 "리턴" 혹은 "대기"를 설정할 수 있다. "대기"란 선 진입한 스레드가 해당영역을 벗어날때까지 후발 스레드는 "대기" 상태로 있다가 선진입한 스레드가 해당영역 벗어나면 대기중인 후발 스레드가 크리티컬 영역을 실행하는것. "리턴"이란 후발 스레드는 "대기"상태에 있지 않고 "리턴"되어 크리티컬 영역 실행 하지 않는 방식이다. 구현방법. - Win32API 함수 또는 MFC 이용가능. |
|
Critical Section - Win32API 함수 이용. |
|
Win32API 함수. 코드예. |
|
Critical Section - MFC 클래스 CCriticalSection 이용. |
|
헤더 : afxmt.h 코드예. |
|
본 글이 포함된 상위 정리 장소. Visual Studio/VC++/C/C# 활용정리 -> http://igotit.tistory.com/11 |
///545
출처: http://igotit.tistory.com/entry/Critical-Section-크리티컬-섹션 [igotit]
=======================
=======================
=======================
출처: http://egloos.zum.com/swain/v/2277941
멀티 스레드를 사용하다 보면 한가지 데이터를 같이 쓸수 있는 경우가 생기게 된다.
이런 경우 데이터가 개발자의 시나리오대로가 아닌 버그가 생길수 있다.
그렇기 때문에 동기화를 하게 된다.
동기화의 방법중 크리티컬 섹션과, 이벤트 방식을 살펴 보겠다.
자세한 설명을 하면 길어 질것 같고 타 블로그에 잘 설명 되어 있으므로 우리가 원하는
사용 API들에 대해서 알아보겠다. (사실 회사라 자세하게 설명까지 적을 시간이 없네요)
크리티컬 섹션, 이벤트 둘중에 하나만 사용하세요... ^^
예)
// 변수 선언은 전역 변수로
// 한개의 쓰레드가 다른 파일에 있다면 다른 파일에서는 extern 으로 선언 하면 되겠죠?
// 헤더파일에 선언해도 되겠고요..
CRITICAL_SECTION cs;
HANDLE hEvent;
// 생성은 쓰레드가 생성되는 곳에서 사용하는게 가장 좋겠지요?
InitializeCriticalSection (&cs); // 크리트컬 섹션 생성
// 두번째 전달값이 FALSE이면 이벤트를 수동이 아닌 자동 모드로 생성하게됩니다.
hEvent = CreateEvent(NULL,TRUE,FALSE,NULL); // 이벤트 생성
DWORD WINAPI Thread1(LPVOID lpParam)
{
for (;;)
{
EnterCriticalSection (&cs); // 크리티컬 섹션에 진입해서 다른 thread에서 크리티컬 섹션에 집입하지 못하게 함
SetEvent(hEvent); // 이벤트를 설정 비신호 상태로 만든다.. 즉 event에 접근 하지 못하도록 막는다.
/*
여기서 할일은 한다.
동기화 예제니깐 타 thread에서도 사용하느 데이터에 접근하는 무슨 일이겠죠?
*/
LeaveCriticalSection (&cs); // 크리티컬 섹션에서 빠져나옴
ResetEvent(hEvent); // 이벤트를 신호 상태로 만든다.
}
DeleteCriticalSection(&cs); // 크리티컬 섹션 파괴
CloseHandle(hEvent); // 이벤트 핸들을 닫는다.(종료)
}
DWORD WINAPI Thread2(LPVOID lpParam)
{
for (;;)
{
EnterCriticalSection (&cs); // 크리티컬 섹션에 진입해서 다른 thread에서 크리티컬 섹션에 집입하지 못하게 함
SetEvent(hEvent); // 이벤트를 설정 비신호 상태로 만든다.. 즉 event에 접근 하지 못하도록 막는다.
/*
여기서 할일은 한다.
동기화 예제니깐 타 thread에서도 사용하느 데이터에 접근하는 무슨 일이겠죠?
*/
LeaveCriticalSection (&cs); // 크리티컬 섹션에서 빠져나옴
ResetEvent(hEvent); // 이벤트를 신호 상태로 만든다.
}
DeleteCriticalSection(&cs); // 크리티컬 섹션 파괴
CloseHandle(hEvent); // 이벤트 핸들을 닫는다.(종료)
}
회사인관계로 여기까지 시간되면 좀더 충실하게 써야 하겠네요..
예전에 동기화때문에 여기저기 찾아다니냐구 힘들었으니..
이 문서 하나로 해결하기 위해서는 설명이 좀 부족한듯 하네요.
-----------------------------------------------------------------------------------------------------------------------
SetEvent(hEvent); // 이벤트를 설정 비신호 상태로 만든다.. 즉 event에 접근 하지 못하도록 막는다.
/
라고 되어 있는데 MSDN을 보면
Sets the state of the event to signaled, releasing any waiting threads.
라고 되어 있습니다.
이벤트 상태를 signaled. 즉 신호상태로 바꾸어 주고, 다른 기다리는 쓰레드를 릴리즈 시켜준다
는군요. 실수하신 것 같아서 써 드립니다.
---------------------------------------------------------------------------------------
지적 감사합니다... 본 글은 전체적으로 수정을 해야겠네요.... 근데... 요새 스레드를 쓸일 없이 단일 스레드 환경에서 일하다보니... 추후 업댓하겠습니다..
=======================
=======================
=======================
출처: http://fist0512.tistory.com/28
Threads and Thread synchronization
Threads
MFC는 2종류의 쓰레드로 구분할 수 있다.
1. user interface threads
메시지 루프가 존재한다. 윈도우를 만들고 이들 윈도우로 보내진 메시지들을 처리한다. 어플리케이션안에 또하나의 어플리케이션(ui-threads)을 만드는것과 비슷하다.일반적으로 별개로 움직이는 다중 윈도우를 만들때 많이 사용되어 진다.
2. worker threads
직접적으로 메시지를 받지 않고 백그라운드에서 동작되기 때문에 윈도우나 메시지루프들이 필요가 없다.
%이 둘간의실질적인 차이는 아직 잘모르겠다. 좀 더 학습하도록
-Creating a Worker Thread
AfxBeginThread함수는 ui-thread,worker thread 둘다 쓰인다. MFC프로그램에서 Win32::CreateThread함수를 사용하지 말아라.
ex)
CWinThread* pThread = AfxBeginThread (ThreadFunc, &threadInfo);
UINT ThreadFunc (LPVOID pParam)
{
UINT nIterations = (UINT) pParam;
for (UINT i=0; i<nIterations; i++);
return 0;
}
CWinThread* AfxBeginThread (AFX_THREADPROC pfnThreadProc,
LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0, DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
nPriority : 쓰레드 우선순위. 쓰레드들 중에서만 상대적인 우선순위를 정할때 사용한다.
nStackSize : 쓰레드의 스택사이즈 라는데 정확히 뭔지..몰라.
dwCreateFlags : 0 일 경우, 이 함수 호출후 바로 쓰레드가 시작되는 것이고,
CREATE_SUSPENDED일 경우, ResumeThread()가 호출되어야지만 시작되는것이다.
lpSecurityAttrs : 몰라도 되.
--Thread Function prototype
콜백함수이기에 static class 멤버 함수이거나 클래스 밖에 선언된 전역함수이어야 한다.
UINT ThreadFunc( LPVOIDpParam )
pParam : 32비트값. AfxBeginThread 함수의 파라미터. 이것은 scalar,handle,객체포인터로도 사용되어 질수 있다.
-Creating a UI Thread
CWinThread를 상속받은 클래스를 사용한다.
ex)
class CMyThread : public CWinThread
CMyThread * pThread = AfxBeginThread(RUNTIME_CLASS(CMyThread),
THREAD_PRIORITY_NORMAL,
0, // stack size
CREATE_SUSPENDED);
pThread->초기값 설정.
pThread->ResumeThread();
또는
CMyThread * pThread = new CMyThread();
pThread->초기값 설정.
pThread->CreateThread();
-Suspending and Resuming Threads
SuspendThread를 호출하면 쓰레드는 멈추고, 내부적으로 Suspend count 가 1 증가 한다. 그리고 ResumeThread를 호출하면 Suspend count는 1 줄고 쓰레드는 다시 움직인다.
ResumeThread는 자신이 호출 할 수 없다. 그리고 이들 함수의 리턴값은 쓰레드의 이전 Suspend Count이다.
-Putting Threads to sleep
::Sleep(0)
현재쓰레드를 중지하고 스케쥴러가 동등한 혹은 높은 우선순위를 갖는 다른 쓰레드를 움직이도록 허락해준다. 만약, 다른 동등한 혹은 높은 우선순위의 쓰레드가 wait 상태가 아니라면, 이 함수는 바로 리턴해서 현재쓰레드를 재시작 한다. (::SwitchToThread.(in NT 4.0 ), ::Sleep(0) (in win32 ))
Sleep함수의 시간은 정확하지 않을 수 있다. 이는 여러환경에 지배받기때문이다. 윈도우즈에서는 쓰레드의 suspend 시간을 보장하는 함수는 존재하지 않는다.
-Terminating a Thread
--Worker Thread
call AfxEndThread.
쓰레드 함수내부의 리턴으로 종료.
--UI Thread
call AfxEndThread.
post WM_QUIT.( ::PostQuitMessage )
(쓰레드 종료함수 사용시 주의할것은 쓰레드 내부에 메모리를 동적할당(new,malloc)해놓고 delete를 안해서 메모리 릭이 날 염려가 있다. 그래서 가급적이면 쓰레드 함수 리턴으로 종료하는 것이 낫다.)
위의 함수들을 호출한 후 한번 제대로 종료됬는지 확인해보라
DWORD dwExitCode;
::GetExitCodeThread (pThread->m_hThread, &dwExitCode);
만약 여전히 살아 있다면 dwExitCode = STILL_ACTIVE(0x103) 으로 되어 있을테다.
- CWinThread 개체 자동 삭제하기
AfxBeginThread 로 스레드 생성할 경우,
CWinThread *pThread = AfxBeginThread( , ,, );
위와 같이 CWinThread 개체 포인터를 반환값으로 받는다. 스레드 종료시, 이 개체는 어떻게 되는가?
MFC는 사용자가 따로 delete pThread; 할 필요없게 자동으로 삭제해 준다.
그런데, 여기서 문제가 있다.
스레드가 종료되지 않았다면, 아래구문은 잘못된 곳이 없다.
::GetExitCodeThread( pThread->m_hThread, &dwExitCode );
( 스레드의 현재 상태값을 반환하는 함수 )
하지만, 종료되어 pThread 개체 포인터 역시 자동으로 삭제되었다면, 위의 구문은 에러를 발생할 것인다.
pThread 는 아무것도 가리 키지 않기 때문에..
해결책)
1. m_bAutoDelete = FALSE; 설정.
-- 이는 곧 사용자가 CWinThread 개체를 delete 해주어야 함을 의미한다.
-- 스레드가 중지가 된 상태에서 m_bAutoDelete = FALSE; 를 설정해주어야 한다.
2. Win32::DuplicateHandle 함수를 호출하여 스레드 핸들의 사본을 생성하여 사용.
--
해당핸들의 참조카운트가 1 -> 2로 증가되어 CloseHandle() 호출시에 다시 2 -> 1 로 감소될뿐 개체는 죽지 않고 남아있다. 물론, 사용자가 명시적으로 CloseHandle() 을 호출해야 한다.
- 다른 스레드 종료하기
1.
//Thread A
nContinue = 1;
CWinThread *pThread = AfxBeginThread( ThreadFunc, &nContinue );
...
...
nContinue = 0 ; // 스레드 B 를 종료해라.
//Thread B
UINT ThreadFunc( LPVOID pParam )
{
int* pContinue = (int*) pParam;
while( *pContinue )
{
....
}
return 0;
}
2. 스레드 A는 스레드 B가 죽을때 까지 무한 기다리도록 하고 싶을 경우,
//Thread A
nContinue = 1;
CWinThread *pThread = AfxBeginThread( ThreadFunc, &nContinue );
HANDLE hThread = pThread->m_hThread; //사본 생성. 스레드B종료시 pThread 없을 경우 대비.
...
...
nContinue = 0 ; // 스레드 B 를 종료해라.
::WaitForSingleObject( hThread, INFINITE );
//Thread B
//1.의 에제와 같음
::WaitForSingleObject 은 hThread 스레드가 종료될때 까지 무한정(INFINITE) 기다리는 함수이다. 반환 값은 아래와 같다.
-- WAIT_OBJECT_0 : 그 개체가 신호를 받았다. 즉, 스레드가 종료되었다.
-- WAIT_TIMEOUT : 스레드는 살아있지만, 시간 만료로 기다리지 않고 반환되었다.
두번째 매개 변수를 0 으로 설정하고 아래와 같이 사용하는 것이 좋다.
if( ::WaitForSingleObject( hThread, 0 ) == WAIT_OBJECT_0 )
//스레드가 더이상 존재치 않는다.
else
//스레드가 여전히 실행중이다.
- ::TerminateThread( hThread, 0 );
다른 스레드를 직접삭제하는 방법은 위의 함수 딱 한가지 존재한다. 어쩔 수 없는 경우에만 사용하도록 .
MFC에서 스레드를 사용하기 전에 알아두어야 할 함수는 AfxBeginThread, AfxEndThread 이렇게 두 함수가 있다. 이 함수에 대한 인자값은 MSDN을 참고하기 바라며, 간단한 사용법을 알아보자.
AfxBeginThread(CalcIt, (LPVOID)val);
첫 번째 인자는 전역함수인데, 클래스에 포함시킬 경우 정적함수로 선언해야되고, 아니면 전역함수로 선언해서 써야한다. CalcIt 함수의 리턴값과 파라미터는 아래와 같다.
UINT CalcIt(LPVOID arg);
꼭 위의 규칙을 따라줘야 한다.
일반적으로는 위와 같이 스레드를 시작하면 된다. 그러면 스레드가 시작되고, CalcIt함수(코어 함수??)의 역할이 끝나면 자동으로 스레드를 종료한다.
하지만 스레드는 위와 같이 쓸 경우 바로 리턴해버린다. 리턴값이 0이면 정상적인 종료를 뜻하게 되는데, 사용자에게 의미있는 리턴값을 받기 위해서, 이렇게 하면 안될것이다.
그래서 사용자가 원하는 값을 리턴 받으려면 아래와 같은 방법을 쓴다.
CWinThread* pThread = AfxBeginThread(CalcIt, (LPVOID)val, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
우선 스레드를 시작하지만 정지상태로 놓는다는 뜻이다.(CREATE_SUSPENDED)
그 다음
pThread->m_bAutoDelete = FALSE;
위에서 말했듯이, 스레드가 시작되고 자기 할일이 끝나면 바로 스레드는 종료된다. 하지만 이 값을 FALSE로 해주면 종료되지 않는다. 대신 사용자가 직접 원하는 시점에 종료를 해줘야 한다.(AfxEndThread)
pThread->ResumeThread();
이제 스레드를 시작한다.
그럼 리턴값을 받아야한다.
::GetExitCodeThread(pThread->m_hThread, &returnvalue); //Get ExitCode from thread.
이 함수는 스레드로 부터 리턴값을 읽어온다. 여기에 포함되는 리턴값을 받기 위해선 위의 CalcIt 함수에 AfxEndThread를 써줘야 한다. AfxEndThread함수의 첫 번째 인자는 위 함수의 returnvalue에 들어갈 리턴값이다. 두번째 인자는 스레듣 객체를 메모리에서 제거할 것인지를 나타낸다. 기본값은 TRUE이고 메모리에서 삭제된다. FALSE이면 메모리에 남아있게 되어 스레드 객체를 재사용 할 수 있다.
그리고 마지막으로 알아둬야 할 것이, WaitForSingleObject 함수이다.
역할은 Sleep과 동일하지만, 이 함수의 첫 번째 인자에 있는 스레드가 사용할 수 있게 될 때까지 기다리는 것이다.
Sleep은 스레드를 멈추는 역할밖에 못하지만, WaitForSingleObject는 특정 시간동안 이벤트를 감지할 수 있습니다.
WaitForSingleObject 의 두 번째 인자에는 밀리세컨드 단위의 시간이 들어가는데, INFINITE가 들어가면 스레드가 끝날때까지 기다리게 됩니다.
만약 구현이 된다면 아래와 같은 구조가 될 것이다.
pThread = AfxBeginThread(ExportVVF, &arg1, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); //Make Thread with suspension
pThread->m_bAutoDelete = FALSE; //if this thread will be end, object will not destory this thread.
pThread->ResumeThread(); //Begin Thread
WaitForSingleObject(pThread->m_hThread, INFINITE);
::GetExitCodeThread(pThread->m_hThread, &returnvalue); //Get ExitCode from thread.
출처: http://fist0512.tistory.com/28 [Personal diary]
=======================
=======================
=======================
출처: http://fist0512.tistory.com/29
쓰레드의 생성, 삭제, 지연등이 잘 정리되어 있다.
출처 : http://manggong.org/rgboard/view.php?&bbs_id=programming3&page=&doc_num=24&PHPSESSID=ad8f0e7c5a75d1e686e415834942cbc2
// 데브피아(devpia) 가욱현, 정대원 님의 글을 토대로 합니다.
1. 개요
현재 대부분의 OS는 프로세스 스케쥴링에 의해 프로그램의 멀티태스킹(Multi-tasking)을 지원하고 있다.
멀티태스킹이란 실행되고있는 프로그램을 일정 단위로 잘라서(slice) 순서대로 CPU를 사용하게끔 하는 것 인데,
사용자는 마치 동시에 여러 개의 프로그램이 실행되는 것처럼 느낄 수 있게 된다.
즉, CPU 사용률을 최대화 하고, 대기시간과 응답시간의 최소화를 가능케 해주는 방법이다.
이번에는 프로세스 한 개만 놓고 보자.
한 프로세스는 구성면에서 [텍스트]-[데이터]-[스택] 영역으로 구성되어있고, 기능면에서는 텍스트의 모듈들은 각각의 역할을 가지고 있다.
프로세스에서의 공유메모리영역을 제외한 부분끼리 묶어서 쓰레드로 만든 후, 이것들을 멀티태스킹처럼 동작시키면 멀티쓰레딩이 되는 것이다.
멀티쓰레드 프로그램을 작성할 경우의 장점은 다음처럼 요약될 수 있다.
1) 병렬화가 증가되어
2) CPU사용률이 극대화되며,
3) 전체적 처리율이 빨라지고,
4) 사용자에대한 응답성이 향상된다.
5) 또한, 완벽에 가까운 기능별 구분에 의한 모듈작성을 함으로써 설계가 단순해져서,
6) 프로그램의 안정성이 향상된다.
7) 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에서 동일한 서비스를 제공할수 있다.
8) 블록될 가능성이 있는 작업을 수행할 때 프로그램이 블록되지 않게 한다.
하지만, 쓰레드를 사용하면 오히려 불리한 경우도 있다. 대표적인 예로, 교착상태(deadlock)와 기아(starvation)이다.
쓰레드 기법을 사용할 때 주의사항을 정리하자면,
1) 확실한 이유를 가지고 있지 않는 경우에는 쓰레드를 사용하면 안 된다. 즉 쓰레드는 명확히 독립적인 경우에 사용해야 한다.
2) 명확히 독립적인 쓰레드라 하여도 오히려 나눔으로 인해 OS가 쓰레드를 다루는데에 따른 부하(overload)가 발생하게 된다.
즉, 실제 쓰레드에 의해 수행되는 작업량보다 클 경우에는 사용하지 않도록한다.
멀티쓰레드를 이용한 애플리케이션을 작성하는 구조에는 3가지 방법이 있다..
1. boss/worker 모델..
2. work crew 모델.
3. pipeline 모델.
1. 첫번째 쓰레드(주쓰레드)가 필요에 따라 작업자 쓰레드를 만들어 내는 경우.
이런 경우는 C/S 환경에서 접속받는 부분을 쓰레드로 돌리고, 접속요청이 오면 새로운 쓰레드를 만들어 사용자와 연결시켜 주는 방법이다.
이때 접속 받는 쓰레드가 주 쓰레드(boss Thread) 라고 하고, 사용자와 연결된 다른 쓰레드..
즉 주 쓰레드로부터 실행된 쓰레드는 작업자 쓰레드(worker Thread) 라고 한다..
2. 두번째 방식은 어떤 한 작업을 여러 개의 쓰레드가 나눠서 하는 방식이다.
즉 집을 청소한다는 개념의 작업이 있으면, 청소하는 작업에 대한 쓰레드를 여러 개 돌리는 거..
3. 공장라인을 생각...
쓰레드는 UI(User Interface) Thread와 Worker(작업자) Thread로 나뉜다.
UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는.. )쓰레드이고..
Worker Thread는, 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수의 경우 사용.
UI Thread를 사용하려면, CWinThread 파생 클래스를 만들어 사용한다.
MFC에서는 AfxBeginThread의 서로 다른 버전 두 개를 정의 하고 있다..
하나는 작업자 쓰레드를 위한 것이고, 하나는 UI쓰레드를 위한 것이져..
원형은 다음과 같다..
UINT ThreadFunc(void* pParam)
이함수는 정적(static)클래스 멤버 함수 이거나 클래스 외부에서 선언한 함수여야 한다.
2. 쓰레드의 기본
1) 쓰레드 생성
WM_CREATE 에서 쓰레드를 만들면 되는데 함수는 다음과 같다.
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter,
DWORD dwCreationFlags, LPDWORD lpThreadId);
+lpThreadAttributes : 쓰레드의 보안속성 지정. 자식 프로세스로 핸들을 상속하지 않은 한 NULL
+dwStackSize : 쓰레드의 스택 크기 지정. 안정된 동작을 위해 쓰레드마다 별도의 스택 할당.
0으로 설정하면 주 쓰레드(CreateThread를 호출한 쓰레드)와 같은 크기를 갖으며, 스택이 부족할 경우 자동으로 스택크기를 늘려주므로 0으로 지정하면 무리가 없다.
+lpStartAddress : 쓰레드의 시작함수를 지정. 가장 중요한 인수.
+lpParameter : 쓰레드로 전달할 작업 내용이되 인수가 없을경우 NULL임.
+dwCreationFlags : 생성할 쓰레드의 특성 지정. 0이면 아무 특성없는 보통 쓰레드가 생성되고
CREATE_SUSPENDED 플래그를 지정하면 쓰레드를 만들기만 하고 실행은 하지 않도록하고 실행을 원하면 ResumeThread함수를 호출하면 된다.
+lpThreadId : 쓰레드의 ID를 넘겨주기 위한 출력용 인수이므로 DWORD형의 변수 하나를 선언한 후 그 변수의 번지를 넘기면 됨.
**** 작업자 쓰레드 생성하기 ****
작업자 쓰레드로 특정한 작업을 하는 사용자 정의 함수를 맹글기 위해서, 윈도우에서는 여러가지 쓰레드 생성 함수를 제공해 준다.
그 함수의 종류를 알아보도록 하져..
1. CreateThread()
2. _beginthread(), _beginthreadex()
3. AfxBeginThread(), AfxBeginThreadEx()
이렇게 약 5가지의 쓰레드 생성함수가 존재한다.
이제부터 저 5가지 함수의 특징을 알아보도록 하져…..
그럼 첫번째 CreateThread()함수. 이 함수는 보통 사용할때 다음과 같이 사용한다.
HANDLE handle;
Handle = CreateThread( Threadfunc(), Param );
첫번째 인자는 사용자가 쓰레드로 돌려야할 작업함수를 써주는 곳이고, 두번째는 작업함수에 인자값으로 전해줄 값이 들어간다..
이 인자값 형은 VOID*으로 되어 있기 때문에 4BYTE 이내의 값은 어떤 값이든 들어갈수 있져..대신 TYPE CASTING을 해주어야 하져..
그리고 받는 쪽에서도 type casting를 해서 받아야 한다.
이함수가 올바르게 실행이 되면 쓰레드에 대한 핸들을 반환하는데.. 이 핸들을 가지고 쓰레드를 조작할 수가 있져..
대표적으로 쓰레드를 닫을 때 CloseHandle()함수를 사용해서 쓰레드 핸들을 넣어주고 쓰레드를 닫아 주어야 한다..
이함수로 생성된 쓰레드를 닫을때는 ExitThread() 면 됩니다.
그럼..두번째 _beginthread를 알아보도록 하져..CreateThread는 쓰레드에서 win32 API함수만 호출할수 있다..
즉, 사용자가 어떤작업을 하는 함수를 만들 때 그 함수 안에서 win32API만 사용할수 있다는 말이다..
즉 C함수나 MFC는 저얼대~~ 못 쓴다….
_beginthread 함수는 win32 API아 C 런타임 함수를 사용할 때 사용한다.
이 함수를 사용하면 C런타임 라이브러리가 핸들을 자동으로 닫으므로 이를 직접할 필요는 없다.
대신 _beginthreadex는 스레드 핸들을 직접 닫아야 한다. 그리고 이 쓰레드를 닫을 때는 _endthread(), _endthreadex()를 사용하면 된다.
세번째 AfxBeginThread()와 AfxBeginThreadEx()..
실질적으로 가장 자주 사용하는 쓰레드 생성함수이다..
이 함수를 이용하면 사용자 정의 함수내에서 MFC, win32 API, C 런타임 라이브러리등 여러가지 라이브러리 함수들을 전부 사용할수 있다..
주로 프로젝트를 MFC로 만들 때 사용하죠..
이 함수는 리턴값이 CWinThread* 형을 리턴하며, 이 함수와 매칭되는 종료함수는 AfxEndThread()이다…
해서 쓰레드가 종료되면 MFC는 쓰레드 핸들을 닫고 리턴값으로 받은 CWinThread*객체를 제거한다.
CWinThread* pThread = AfxBeginThread( Threadfunc, &threadinfo );
첫번째 인자는 사용자 정의 함수이고, 두번째는 첫번째 인자의 쓰레드 함수에 인자값으로 들어갈 파라미터이다..
이 형은 void* 형으로 4byte를 가지므로 어떤 형으로 넣어줄 때 type casting하면 된다….
그 예는 다음과 같다.
int nNumber = 1000;
CWinThread *pThread = ::AfxBeginThread(ThreadFunc, &nNumber);
UINT ThreadFunc(LPVOID pParam)
{
int j = (int)pParam;
for (int i=0; i<j; i++)
{
// 수행할 작업
}
}
작업자 스레드 함수에 4바이트 이상의 정보를 넘겨주어야 할 경우에는
다음과 같이 작업자 스레드 함수에 넘겨주어야 할 모든 값을 포함하는 구조체를 선언하고,
typedef struct tagTREADPARAMS {
CPoint point;
BOOL *pContinue;
BOOL *pFriend;
CWnd *pWnd;
} THREADPAPAMS;
// 그런 다음 구조체에 필요한 값들을 설정하고, 이 구조체의 포인터를 넘겨준다.
THREADPAPAMS *pThreadParams = new THREADPAPAMS; // new로 할당
pThreadParams->point = m_ptPoint;
pThreadParams->pContinue = &m_bExec; // 쓰레드 실행 플래그
pThreadParams->pFriend = &m_bYield; // 쓰레드 양보 플래그
pThreadParams->pWnd = this;
m_pThread = AfxBeginThread(ThreadFunc, pThreadParams);
UINT ThreadFunc(LPVOID pParam)
{
// 넘어온 인자를 복사
THREADPAPAMS *pThreadParams = (THREADPAPAMS *)pParam;
CPoint point = pThreadParams->point;
CWnd *pWnd = pThreadParams->pWnd;
BOOL *pContinue = pThreadParams->pContinue;
BOOL *pFriend = pThreadParams->pFriend;
delete pThreadParams; // delete로 해제
// "실행" 플래그가 TRUE인 동안 스레드가 실행됨
while(*pContinue)
{
// 수행할 작업
// "양보" 플래그가 TRUE이면 다른 스레드에 CPU를 양보
if(*pFriend) Sleep(0);
}
return 0;
}
자 그럼..정리해 보도록 하져…..쓰레드를 생성하는 함수들은 크게 3가지가 있고..(확장된것까지 생각하면 5개..^^ ) 이들 함수의 특징은 다음과 같다.
쓰레드가 win32 API만을 사용한다면 CreateThread()를 사용하면 되고, C런타임 라이브러리를 사용하다면 _beginthread()를 사용하고,
전부다 사용한다면 AfxBeginThread()를 사용하면 된다.
2) 쓰레드 종료
작업 쓰레드가 종료되었는지 조사하는 함수는 다음과 같다.
BOOL GetExitCodeThread(HANDLE hThread, PDWORD lpExitCode);
+hThread : 쓰레드의 핸들
+lpExitCode : 쓰레드의 종료코드.
+Return : 계속 실행중 : STILL_ACTIVE, 쓰레드 종료 : 스레드 시작함수가 리턴한 값 or ExitThread 함수의 인수
쓰레드가 무한루프로 작성되어 있다해도 프로세스가 종료되면 모든 쓰레드가 종료되므로 상관이 없다.
백그라운드 작업을 하는 쓰레드는 작업이 끝나면 종료되는데 때로는 작업도중 중지해야 할 경우에는 다음 두 함수가 사용된다.
VOID ExitThread(DWORD dwExitCode);
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
ExitThread는 스스로 종료할 때 사용.인수로 종료코드를 넘김. 종료코드는 주 쓰레드에서 GetExitCodeThread함수로 조사할 수 있다.
이것이 호출되면 자신의 스택을 해제하고 연결된 DLL을 모두 분리한 후 스스로 파괴된다.
TerminateThread는 쓰레드 핸들을 인수로 전달받아 해당 쓰레드를 강제종료시킨다.
이 함수는 쓰레드와 연결된 DLL에게 통지하지 않으므로 DLL들이 제대로 종료처리를 하지 못할 수 있고 리소스도 해제되지 않을 수 있다.
그래서 이 작업 후 어떤일이 발생할지를 정확히 알때에만 사용하도록한다.
스레드를 죽이는 방법엔 두가지가 있져..
1. 스레드 내부에서 return을 시킬 때.
2. AfxEndThread를 호출할 때.
안전한 방법은 스레드 내부 자체에서 return문을 이용해서 죽여주는게 안전하다. 위의 예와 같이...
다음은 쓰레드를 종료하는 함수의 예이다.
if(m_pThread != NULL)
{
HANDLE hThread = m_pThread->m_hThread; // CWinThread *m_pThread;
m_bExec = FALSE; // 실행 플래그를 FALSE로 하여 쓰레드 종료시킴..
::WaitForSingleObject(hThread, INFINITE);
// 이후 정리작업...
}
위의 첫번째 방법과 같이 return을 받았을때는 GetExitCodeThread를 이용해서 검색할수 있는 32bit의종료 코드를 볼수 있다..
DWORD dwexitcode;
::GetExitCodeThread( pThread->m_hThread, &dwExitCode );
// pThread는 CWinThread* 객체의 변수..
만약 실행중인 스레드를 대상으로 저 코드를 쓰게 된다면 dwExitCode에는 STILL_ACTIVE라는 값이 들어가게 된다.
근데..위의 코드를 사용함에 있어 제약이 좀 있다.
CWinThread*객체는 스레드가 return 되어서 종료가 되면 CWinThread객체 자신도 제거되어 버린다..즉 동반자살이져..
delete시켜주지 않아도 메모리에서 알아서 없어진다는 말이져..
즉…return이 되어서 이미 죽어버린 스레드를 가지고 pThread->m_hThread를 넣어주면, Access위반이란 error메시지가 나오게 되져..
이런 문제를 해결할라면 CWinThread* 객체를 얻은 다음 이 객체의 멤버 변수인 m_hAutoDelete를 FALSE로 설정하면
스레드가 return을 해도 CWinThread객체는 자동으로 제거 되지 않기 때문에 위의 코드는 정상적으로 수행이 된다..
이런 경우에 CWinthread*가 더 이상 필요가 없어지면 개발자 스스로 CWinThread를 delete시켜 주어야 한다.
또다른 방법으로 스레드가 가동이 되면 CWinThread*의 멤버변수인 m_hThread를 다른 곳으로 저장을 해놓고
이 것을 직접GetExitCodeThread()에 전달을 하면 그 쓰레드가 실행중인지 한때는 실행되고 있었지만 죽어버린 스레드인지 확인이 가능하다.
int a = 100; // 파라미터로 넘겨줄 전역변수.
CWinThread* pThread // 전역 쓰레드 객체의 포인터 변수.
HANDLE threadhandle; // 스레드의 핸들을 저장할 핸들변수.
Initinstance() // 프로그램초기화.
{
// 프로그램 실행과 동시에 스레드 시작.
1번방법:pThread = AfxBeginThread( func, (int) a );
// 스레드가 리턴되면 자동으로 CWinThread객체가 자동으로 파괴되지 않게 설정.
2번방법:pThread->m_hAutoDelete = FALSE;
// 쓰레드 핸드를 저장. 위의 m_hAutoDelete를 설정하지않았을경우..
threadhandle = pThread->m_hThread;
}
MessageFunction() // 어떤 버튼을 눌러서 스레드의 상태를 알고 싶다..
{
char* temp;
DWORD dwExitcode;
// 스레드 객체의 m_hAutoDelete를 fasle로 설정해서 스레드가 return되어도
// 객체가 자동으로 파괴되지 않아서 핸들을 참조 할수 있다.
1번방법: ::GetExitCode( pThread->m_hThread, &dwExitcode);
// 스레드가 종료되고 미리 저장해둔 핸들을 이용할경우..
2번방법:::GetExitCode(threadhandle, &dwExitcode);
sprintf( temp, "Error code : %d", dwExitcode );
// 스레드 객체 삭제..
1번방법: delete pThread;
AfxMessageBox( temp );
}
func( void* pParam )
{
int b = (int) pParam;
for( int i = 0; i < b; i++)
{
// 어떤일을 한다.
}
return; // 작업이 끝나면 리턴한다. 이때 스레드 자동으로 종료.
}
1번째 방법은 스레드를 생성하고 m_hAutoDelete를 false로 해서
스레드가 return해서 자동종료해도 CWinthread를 자동파괴하지 않게 하고, GetExitCodeThread()를 호출하져..
밑에서 delete해 주는 거 꼭 해야되고요..안그럼 메모리 누수가 되져..
2번째는 m_hThread를 다른 핸들변수에 저장해 놓고..스레드가 return되면 CWinThread*도 같이 파괴가 되는데..
원래 저장한 핸들을 가지고 GetExitcodeThread()를 호출해서 한때 존재했지만 종료된 쓰레드를 검사하는 것이져….이해 OK?????
3) 대기 함수
WaitForSingleObject(), WaitForMultipleObjects()의 원형은 다음과 같다.
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in array
CONST HANDLE *lpHandles, // object-handle array
BOOL bWaitAll, // wait option
DWORD dwMilliseconds // time-out interval
);
쓰레드 종료를 위한 플래그를 설정한 후, 쓰레드가 완전히 종료된 것을 확인 후에 어떤 작업을 하고 싶으면 다음과 같이 한다.
if (::WaitForSingleObject(pThread->m_hThread, INFINITE))
{
// 쓰레드가 종료된 후 해야 할 작업들
}
(쓰레드 종료를) 어느 정도 기다리다가 프로그램을 진행시키려면 다음과 같이 한다.
DWORD dwRetCode;
dwRetCode = ::WaitForSingleObject(pThread->m_hThread, 2000);
if (dwRetCode == WAIT_OBJECT_0)
{
// 쓰레드가 종료된 후 해야 할 작업들
}
else if(dwRetCode == WAIT_TIMEOUT)
{
// 2초 동안 쓰레드가 종료되지 않았을 때 해야 할 에러 처리
}
다음과 같이 하면, 어떤 쓰레드가 현재 실행 중인지 아닌지를 알 수 있다.
if (::WaitForSingleObject(pThread->m_hThread, 0) == WAIT_TIMEOUT)
{
// 현재 쓰레드가 실행 중.
}
else
// 실행 중인 상태가 아니다.
// WaitForMultipleObjects() sample...
// 쓰레드 함수의 원형
DWORD WINAPI increment(LPVOID lParam);
DWORD WINAPI decrement(LPVOID lParam);
int main()
{
// char* ps[] = {"increment", "decrement"};
DWORD threadID;
HANDLE hThreads[2];
// hThreads[0] = CreateThread( NULL, 0, increment, (LPVOID)ps[0], 0, &threadID);
// hThreads[0] = CreateThread( NULL, 0, increment, NULL, 0, &threadID);
for (int i=0; i<2; ++i)
{
hThreads[i] = CreateThread( NULL, 0, increment, (void *)i, 0, &threadID);
}
// 모든 쓰레드가 종료할 때 까지 기다린다.
// WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
int ret;
ret = WaitForMultipleObjects(2, hThreads, FALSE, INFINITE);
switch(ret)
{
case WAIT_OBJECT_0: // handle hThreads[0] is signaled..
break;
case WAIT_OBJECT_0+1:
break;
}
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
}
DWORD WINAPI increment(LPVOID lParam)
{
while (1)
{
...
}
return 0;
}
DWORD WINAPI decrement(LPVOID lParam)
{
while (1)
{
...
}
return 0;
}
4) 쓰레드 일시중지 - 재개
DWORD SuspendThread(HANDLE hThread); - 1
DWORD ResumeThread(HANDLE hThread); - 2
둘 다 내부적으로 카운터를 사용하므로 1을 두번 호출했다면 2도 두번 호출해야한다. 그래서 카운터가 0 이되면 쓰레드는 재개하게된다.
5) 우선순위 조정
향상된 멀티태스킹을 지원하기 위해서는 시분할 뿐만 아니라 프로세스의 우선순위를 지원해야 한다.
마찬가지로 프로세스 내부의 쓰레드들도 우선순위를 갖아야 하며 우선순위 클래스, 우선순위 레벨 이 두 가지의 조합으로 구성된다.
우선순위 클래스는, 스레드를 소유한 프로세스의 우선순위이며
CreateProcess 함수로 프로세스를 생성할 때 여섯번째 파라미터 dwCreationFlag로 지정한 값이다.
디폴트는 NORMAL_PRIORITY_CLASSfh 보통 우선순위를 가지므로 dwCreationFlag를 특별히 지정하지 않으면 이 값이 전달된다.
우선순위 레벨은 프로세스 내에서 쓰레드의 우선순위를 지정하며 일단 쓰레드를 생성한 후 다음 두 함수로 설정하거나 읽을 수 있다.
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
Int GetThreadPriority(HANDLE hThread);
지정 가능한 우선순위 레벨은 총 7가지 중 하나이며 디폴트는 보통 우선순위인 THREAD_PRIORITY_NORMAL 이다.
우선순위 클래스와 레벨값으로부터 조합된 값을 기반우선순위(Base priority)라고 하며 쓰레드의 우선순위를 지정하는 값이 된다.
기반우선순위는 0~31 중 하나이며 0은 시스템만 가질 수 있는 가장 낮은 우선순위 이다. (낮을수록 권한이 높음)
우선순위를 높이는(에이징)방법과 낮추는 방법을 동적 우선순위 라고하며, 우선순위 부스트(Priority Boost)라고 한다.
단 이 과정은 기반 우선순위 0~15 사이의 쓰레드에만 적용되며 16~31 사이의 쓰레드에는 적용되지 않는다.
또한 사용자입력을 받거나(인터럽트) 대기상태에서 준비상태가 되는 경우에는 우선순위가 올라가고,
쓰레드가 할당된 시간을 다 쓸 때마다 우선순위를 내려 결국 다시 기반 우선순위와 같아지게 되는데,
어떠한 경우라도 동적 우선순위가 기반 우선순위보다는 더 낮아지지 않는다.
3. 쓰레드간 동기화
멀티쓰레드는 개요에서 말했듯이 한 프로세스를 여러 역할에 따라 여러 개의 쓰레드로 나뉘어 작업하는 방식이므로 각 쓰레드간의 동기화가 필요하다.
동시에 복수개의 코드가 같은 주소영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번하기 때문이다.
멀티쓰레드의 가장 큰 문제점은 공유자원(주로 메모리의 전역변수)을 보호하기가 어렵다는 점이다.
그리고 쓰레드간의 실행순서를 제어하는 것도 쉽지 않은 문제이다.
이런 여러가지 문제점을 해결하기 위하여 쓰레드간의 실행 순서를 제어할 수 있는 여러가지 방법들을 동기화라고 한다.
동기화 방법에는, Interlocked, 임계영역, 뮤텍스, 세마포어, 이벤트등의 기법을 사용한다.
1) 임계영역 (Critical Section)
동기화문제를 해결하는 방법들 중 가장 쉬운반면 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
임계영역(Critical Section)이란 공유자원의 독점을 보장하는 코드의 영역을 가리킨다. 이는 아래 두 함수로 시작하고 끝낸다.
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
CRITICAL_SECTION형의 포인터형은 복수개의 쓰레드가 참조해야 하므로 반드시 전역변수로 선언해야한다. 사용법은 다음과 같다.
CRITICAL_SECTION crit1, crit2;
함수 {
…
EnterCriticalSection(&crit1);
//공유자원1을 액서스한다.
LeaveCriticalSection(&crit1);
EnterCriticalSection(&crit2);
//공유자원2을 액서스한다.
LeaveCriticalSection(&crit2);
…
}
주의할것은 가급적 임계영역 내부의 코드가 빨리 끝날 수 있도록 짧은 시간을 사용하도록 작성해야 한다.
만약 Leave를 호출하지않고 쓰레드를 빠져나와버리면 이후부터는 다른 쓰레드는 이 임계영역에 들어갈 수 없게된다.
만약 이부분에서 예외가 발생하여 Leave함수가 호출되지 못하게 될 수도 있다.
그래서 임계영역을 쓸 때는 반드시 구조적 예외 처리구문에 포함시켜주는 것이 좋다.
Try {
EnterCriticalSection(&crit);
…
}
finally {
LeaveCriticalSection(&crit);
}
이렇게하면 설사 예외가 발생하더라도 Leave함수는 반드시 호출되므로 훨씬 안전해진다.
다음은 MFC 에서의 사용 예이다.
CCriticalSection g_critical; // 전역 변수로 선언
function()
{
AfxBeginThread(ThreadFuncA, NULL);
AfxBeginThread(ThreadFuncB, this);
}
UINT ThreadFuncA(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncA가 할 일....
g_critical.Unlock();
}
return 0;
}
UINT ThreadFuncB(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncB가 할 일....
g_critical.Unlock();
}
return 0;
}
2) 뮤텍스(Mutex)
임계영역은 앞서 말했듯 동일한 프로세스 내에서만 사용할 수 있다.
그러나, 뮤텍스(Mutex; Mutual Exclusion;상호배제)는 임계영역이 사용된 곳에 대신 사용될 수 있으며, 프로세스 간에도 사용할 수 있다.
뮤텍스를 사용하려면 다음 함수로 생성해야 한다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL blInitialOwner, LPCTSTR lpName);
lpMutexAttributes : 보안속성. 대개 NULL
blInitialOwner : 뮤텍스 생성과 동시에 소유할 것인지 지정.
lpName: 뮤텍스의 이름을 지정하는 문자열.
뮤텍스는 프로세스간의 동기화에도 사용되므로 이름이 필요하고, 이 이름은 프로세스간 뮤텍스를 공유할 때 사용된다.
뮤텍스 소유를 해지하여 다른 쓰레드가 이것을 가질 수 있도록 하려면 임계영역의 LeaveCriticalSection 에 해당하는 다음 함수를 호출하면 된다.
BOOL ReleaseMutex(HANDLE hMutex);
만일 프로세스가 다른 프로세스의 쓰레드에 의해서 이미 생성된 뮤텍스의 핸들을 얻기를 원하거나,
뮤텍스가 존재하지 않는 경우에 뮤텍스를 생성하기 원한다면 다음 함수를 사용한다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
3) 세마포어 (Semaphore)
세마포어도 뮤텍스와 유사한 동기화 객체이나 다른점은, 뮤텍스는 하나의 공유자원을 보호하기 위해 사용하지만,
세마포어는 제한된 일정 개수를 가지는 자원(HW, 윈도우, 프로세스, 쓰레드, 권한, 상태 등 컴퓨터에서의 모든 자원)을 보호하고 관리한다.
세마포어는 사용 가능한 자원의 개수를 카운트하는 동기화 객체이다.
세마포어와 관련된 함수는 다음과 같다.
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG IlInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
4) 이벤트 (Event)
임계영역, 뮤텍스, 세마포어는 주로 공유자원을 보호하기 위해 사용되는 데 비해
이벤트는 이보다는 스레드간의 작업순서나 시기를 조정하기 위해 사용한다.
특정한 조건이 만족될 때까지 대기해야 하는 쓰레드가 있을 경우 이 쓰레드의 실행을 이벤트로 제어할 수 있다.
이벤트는 자동리셋과 수동리셋이 있다.
+자동 리셋 이벤트 : 대기상태가 종료되면 자동으로 비신호상태가 된다.
+수동 리셋 이벤트 : 쓰레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.
++신호상태 (Signaled): 쓰레드 실행가능상태. 신호상태의 동기화 객체를 가진 쓰레드는 계속 실행할 수 있다.
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
bManualReset은 이벤트가 수동리셋 이벤트(manual)인지 자동리셋 이벤트(automatic)인지 지정하는데 TRUE이면 수동리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 쓰레드가 곧바로 실행을 하도록 해준다.
이벤트도 이름(lpName)을 가지므로 프로세스간의 동기화에 사용될 수 있다.
또한 이벤트가 임계영역이나 뮤텍스와 다른점은
대기함수를 사용하지 않고도, 쓰레드에서 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다. 다음 함수를 사용한다.
BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);
SetEvent는 신호상태로 만들고 ResetEvent는 비신호상태로 만든다.
다음은 MFC 에서의 사용 예이다.
CEvent g_event; // 전역변수로 선언
FunctionA()
{
AfxBeginThread(ThreadFunc, this);
}
FunctionB()
{
g_event.SetEvent(); // Lock() 함수에서 더 이상 진행하지 못하고 잠자고 있는 쓰레드를 깨워서 일을 시키려면 SetEvent()를 호출.
}
// ThreadFunc() 함수는 이벤트가 발생할 때마다 while문을 한번씩 실행.
UINT ThreadFunc(LPVOID pParam)
{
while(1)
{
g_event.Lock(); // SetEvent()가 호출되면, Lock()함수에서 실행이 중단된 쓰레드가 다음 코드를 실행.
// ThreadFunc가 할 일....
g_event.Unlock();
}
return 0;
}
출처: http://fist0512.tistory.com/29 [Personal diary]
=======================
=======================
=======================
출처: http://fist0512.tistory.com/30
유닉스랑 리눅스의 pthread만 쓰다가
과제가 나왔길래 리눅스로 할래 MFC로 할래 하길래 이번 기회에 MFC 쓰레드도 공부할겸
MFC로 하기로 했다.
MFC 쓰레드를 이용하는 것이 처음이라서 먼저 서너시간쯤 MFC쓰레드에 관해 살펴보다가 짰다.
처음에 win 32api쓰레드와 MFC 쓰레드가 헷갈려서 찾아보다가 결론은 두 개는 거의 비슷하고
MFC 쓰레드는 win32 api 쓰레드를 wrapping 한 것이라고 S모씨가 알려주었다.
아 다만 쓰레드 생성 함수가 약간 다르다.
MFC는 쓰레드가 Worker 쓰레드와 user interface 쓰레드로 나뉘고 각각의 쓰임새가 있다.
처음에 뭘 쓸까, 무슨 차이일까 고민하다가 찾아보니
UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는.. )쓰레드이고..
Worker Thread는, 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수의 경우 사용. UI Thread를 사용하려면, CWinThread 파생 클래스를 만들어 사용한다.
라는 똑같은 결론들만 있길래 좀 더 알아보니
심민조 ( [민조] I saw U.. ) 님의 말 :흠 잠시만심민조 ( [민조] I saw U.. ) 님의 말 :ㅎㅎㅎ심민조 ( [민조] I saw U.. ) 님의 말 :단순히 설명을 보자면심민조 ( [민조] I saw U.. ) 님의 말 :쓰레드의 개념은 같은데심민조 ( [민조] I saw U.. ) 님의 말 :별개의 존재로 생성되서 그리로 메시지가 전달되느냐심민조 ( [민조] I saw U.. ) 님의 말 :아니면심민조 ( [민조] I saw U.. ) 님의 말 :그냥 현재 app의 백그라운드에서심민조 ( [민조] I saw U.. ) 님의 말 :돌아가게 되느냐심민조 ( [민조] I saw U.. ) 님의 말 :뭐 그런차이인듯한데방호남 ( 최-악이다 ) 님의 말 :네 그런거 같아요심민조 ( [민조] I saw U.. ) 님의 말 :좀더 자세히보면심민조 ( [민조] I saw U.. ) 님의 말 :사실상 OS에서는심민조 ( [민조] I saw U.. ) 님의 말 :둘다 그냥 쓰레드로 인식하지만심민조 ( [민조] I saw U.. ) 님의 말 :MFC클래스레벨에서심민조 ( [민조] I saw U.. ) 님의 말 :사용자편의를 위해심민조 ( [민조] I saw U.. ) 님의 말 :구분한거라고하네
즉 별개의 메시지 창을 가지고 메시지 창을 따로 띄우고 싶으면 UI 쓰레드를,
그냥 백그라운드에서 작업하려면 worker 쓰레드를 쓴다.. 라고 간단히 정리 된다.
여하간 몇시간동안 살펴보다가 코딩을 했고 실제 코딩시간은 한 2시간? 쯤 걸렸다.
코드는 다음과 같다. 복 붙 해도 실행은 안 될 것이다. 대략적인 흐름만 기억해두기 위해
헤더파일과 다른 파일은 붙여넣지 않고 메인 코드만 붙여넣었으니.
조금 허접하다. 느낌이 뭔가 inefficient하다.
#include "stdafx.h"
#include "Resource.h"
#include "MyThread.h"
#include <process.h>
#include <stdlib.h>
#include <windows.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
typedef int buffer_item;
#define BUFFER_SIZE 5
//CWinApp theApp;
using namespace std;
static int in=0; //0으로 초기화
buffer_item buffer[BUFFER_SIZE];
HANDLE hmutex,psema,csema; //생산 또는 소비 할때 1개 쓰레드만 작업 할 수 있게 하기 위해 mutex로 임계영역을 설정하고, 버퍼 사이즈 만큼
//쓰레드가 생산할 수 있게 하도록 하기 위해 psemaphore를 이용하여 초기값을 BUFFER_SIZE 만큼 할당하였다.
//생산자 쓰레드가 버퍼를 다 채우면, csemaphore를 통해 소비자쓰레드가 버퍼를 소비하도록 하였다.
CWinThread *pThread[50]; //Thread pool. 미리 50개쯤 잡아둔다
CWinThread *cThread[50]; //Thread pool. 미리 50개쯤 잡아둔다
int isWorking=1;
int insert_item(buffer_item item) {
buffer[in]=item;
in=(in+1)%BUFFER_SIZE;
return 0;
}
int remove_item(buffer_item *item) {
if( in !=0 ){ //생산된 상품이 있다면
*item=buffer[in-1];
buffer[in-1]=0;
in--;
}
else if ( (in==0) && (buffer[BUFFER_SIZE -1] !=0) ) { //상품 버퍼가 모두 다 찼다면
*item=buffer[BUFFER_SIZE-1];
buffer[BUFFER_SIZE-1]=0;
in=BUFFER_SIZE-1;
}
return 0;
}
UINT producer(void *pData)
{
int sleepTime=0;
buffer_item item;
while(isWorking==1) {
//********************************임계영역 설정***********************************
WaitForSingleObject(psema, INFINITE);
WaitForSingleObject(hmutex, INFINITE);
sleepTime=rand()%3000; //임의의 시간으로 쉬대 최대 3초까지만 쉰다
Sleep(sleepTime);
item=rand()+1;
printf("producer produced %d \n",item);
insert_item(item);
ReleaseMutex(hmutex);
ReleaseSemaphore(csema,1,NULL);
//********************************임계영역 설정***********************************
}
return 1;
}
UINT consumer(void *pData)
{
int sleepTime=0;
buffer_item item=0;
while(isWorking==1) {
//********************************임계영역 설정***********************************
WaitForSingleObject(csema,INFINITE); //버퍼가 다 차면!
WaitForSingleObject(hmutex, INFINITE);
sleepTime=rand()%3000; //임의의 시간으로 쉬대 최대 3초까지만 쉰다
Sleep(sleepTime);
remove_item(&item);
printf("consumer consumed %d \n", item);
ReleaseMutex(hmutex);
ReleaseSemaphore(psema,1,NULL);
//********************************임계영역 설정***********************************
}
return 1;
}
int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
hmutex=CreateMutex(NULL, FALSE, NULL);
psema=CreateSemaphore(NULL,BUFFER_SIZE,BUFFER_SIZE,NULL);
csema=CreateSemaphore(NULL,0,1,NULL);
int time=2,numProducer=1,numConsumer=1; //기본값으로 1초실행, 생산자, 소비자 쓰레드 1개씩 할당.
printf("Please enter the whole work time(second). \n");
scanf("%d", &time);
time*=1000;
printf("Please enter the number of producer thread \n");
scanf("%d", &numProducer);
printf("Please enter the number of consumer thread \n");
scanf("%d", &numConsumer);
for(int i=0; i<numProducer ; i++)
pThread[i] = AfxBeginThread(producer,
THREAD_PRIORITY_NORMAL,
0, // stack size
CREATE_SUSPENDED);
for(int i=0; i<numConsumer ; i++)
cThread[i] = AfxBeginThread(consumer,
THREAD_PRIORITY_NORMAL,
0, // stack size
CREATE_SUSPENDED);
Sleep(time);
isWorking=0; //작업 시간 종료
printf("Time is over.... done!! \n");
return 0;
}
출처: http://fist0512.tistory.com/30 [Personal diary]
=======================
=======================
=======================
쓰레드 동기화 오브젝트
(Thread Synchronization Objects)
쓰레드가 2개 이상 실행될 때 여러가지 변수 가있습니다.
하나의 공유자원(예를들어 동시에 접근하는 변수) 에 접근할 때, 파일 입출력 이나 디바이스I/O작업을 할 때 동기화 오브젝트가 필요합니다.
동기화 오브젝트 없이 쓰레드가 공유 자원을 사용할 때 공유자원이 원치 않은 값이 될수 있고, I/O작업 시 쓰레드가 I/O작업이 끝날 때 까지 무한정 블로킹(blocking : 특정 함수가 리턴 될 때 까지 기다림)현상이 발생할 수 있습니다.
동기화 오브젝트를 사용하여 다중 쓰레드에서 어떻게 안전 하게 공유자원에 접근하고 다른 쓰레드간의 실행 순서등을 조작하는지에 대해 알아 보겠습니다.
동기화 오브젝트는 유저 모드와 커널 모드로 분류할 수 있습니다.
유저 모드
유저 모드는 현재 프로세스/쓰레드 내의 상태를 말합니다.
유저모드에서는 커널 오브젝트(프로세스, 파일, 디바이스 등)으로 바로 접근을 할 수 없고, 커널 오브젝트로 접근 시 시스템에 의해 변환 작업이 이루어집니다.
이런 변환 작업은 시간을 많이 걸리는 작업이기 때문에 유저 모드가 커널 모드 보다 속도가 빠릅니다.
유저 모드 동기화 방법은 코드레벨에서 동기화 하는 방법을 이야기 합니다.
유저모드에서 동기화는 사용하기 쉽고 커널 모드 동기화 함수들에 비해 속도가 빠른 장점이 있지만 커널 오브젝트(파일 I/O, 프로세스, 쓰레드 등)의 동기화는 불가능하다는 단점이 있습니다.
Interlocked
Interlocked 함수들은 다중 쓰레드에서 공유변수들을 안전하게 1씩 증가/감소, 특정값을 증가 , 비트 연산을 할 수 있습니다.
Intlocked 함수는 사용하기 쉬우므로 길게 설명은 하지 않겠습니다.
아래 사이트를 참조하시길 바랍니다.
http://msdn2.microsoft.com/en-us/library/ms686360(VS.85).aspx
크리티컬 섹션(CRITICAL SECTION)
크리티컬 섹션은 특정 코드영역을 쓰레드가 동시에 실행되는 것을 막아 줍니다.
아래는 크리티컬 섹션 관련 함수들입니다.
DeleteCriticalSection | 크리티컬 섹션 오브젝트를 삭제 합니다. |
EnterCriticalSection | 특정 크리티컬 섹션권한을 가질 때 까지 기다립니다.(Wait) |
InitializeCriticalSection | 크리티컬 섹션 오브젝트를 초기화 합니다. |
InitializeCriticalSectionAndSpinCount | 크리티컬 섹션 오브젝트를 초기화 하고 스핀 카운트를 설정합니다. |
InitializeCriticalSectionEx | 크리티컬 섹션을 초기화하고 스핀카운트 설정, 부가기능을설정합니다. |
LeaveCriticalSection | 크리티컬 섹션 오브젝트 권한을 해제합니다.. |
SetCriticalSectionSpinCount | 특정 크리티컬 섹션 오브젝트 스핀카운트를 설정합니다. |
TryEnterCriticalSection | 블로킹(Wait) 되지 않고 크리티컬 섹션 오브젝트의 권한을요청합니다. |
InitializeCriticalSectionAndSpinCount 함수는 스핀 카운트를 두어서 쓰레드가 크리티컬 섹션 오브젝트를 획득하지 못하면 Wait상태로 일정 시간(스핀카운트) 루프를 돌아 크리티컬 섹션오브젝트가 해제 되었는지 체크합니다.
해제되지 않았으면 Sleep하게 됩니다.
이 함수는 멀티 프로세서 환경에서만 유효하며, 스핀카운터는 4000을 추천(Windows Via C/C++)하지만 자신의 환경에서 값을 바꾸어 가며 테스트 해보길 권장합니다.
#include <Windows.h> CRITICAL_SECTION g_cs; //크리티컬 섹션 오브젝트를 초기화 합니다. void InitCriticalSection() { InitializeCriticalSection(&g_cs); } unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler) { while (bExit == FALSE) { if(TryEnterCriticalSection(&g_cs)) { //쓰레드 작업을 수행 합니다. //수행 하고 LeaveCriticalSection 함수를 호출 //하여 크리티컬섹션 오브젝트를 해제합니다. LeaveCriticalSection(&g_cs); } else { //크리티컬 섹션 오브젝트 획득에 실패시 수행할 // 작업을 선언합니다. //SwitchToThread함수를 호출하여 다른 쓰레드로 // 스위칭 합니다. SwitchToThread(); } } DWORD exitCode; GetExitCodeThread(InputThrd, &exitCode); _endthreadex(exitCode); return 0; } |
**Sleep() 함수와 SwitchToThread()함수는 디스패쳐가 다른 쓰레드로 스케쥴 하도록 합니다.
차이점은 Sleep()함수는 현재 쓰레드보다 우선순위가 같거나 높은 쓰레드가 없으면 쓰레드 전체를 리스케쥴링(rescheduling)합니다.
Slim Reader/Writer Locks
Slim reader/writer (SRW) locks 는 하나의 프로세스내의 쓰레드들이 공유자원을 동기화 할수 있습니다.
아주 작은 메모리를 차지하면서 속도도 빠릅니다.
Reader 쓰레드는 공유자원을 읽고 Writer 쓰레드는 공유자원에 쓰기 작업을 할수 있습니다.
다중 쓰레드가 공유자원을 읽고 쓰기를 할 때, 크리티컬 섹션과 뮤텍스 같은 상호배제오브젝트(exclusive locks)들은 reader 쓰레드는 계속 돌고 writer 쓰레드는 거의 돌지못하면 병목현상(bottle neck)이 발생 할 수 있습니다.
SRW locks는 공유자원에 접근 할수 있는 두가지 모드를 제공합니다. :
· Shared mode : 읽는 작업을 하는 쓰레드가 여러 개일 때공유자원을 읽기 전용으로 접근할 수 있도록 해서 동시 다발적으로작업을 할수 있도록 합니다. 만약 읽는 작업이 쓰는 작업을 초과 할경우, 성능과 처리량은 크리티컬 섹션과 동일하게 됩니다.
· Exclusive mode : 읽기/쓰기 쓰레드는 하나의 쓰레드만 접근 할 수있습니다. Exclusive mode로 락이 걸려지면 다른 쓰레드들은공유자원에 접근할 수 없습니다.
하나의 SRW lock은 두가지 모드를 동시에 가질수 있습니다. 읽는 쓰레드는 Shared mode로 쓰는 쓰레드는 Exclusive mode로 작업을 할 수 있습니다. 어떤 쓰레드가 소유권을 먼저 가질지는 알수 없습니다. SRW Locks는 공정하거나 선입선출(First In First Out : FIFO)방식이 아닙니다.
SRW lock은 포인터 크기를 가집니다. 장점은 속도가 빠르고 lock상태의 변환이 빠르다는 것 입니다.
단점은 아주 작은 상태 정보만 저장이 되어 재귀적으로 SRW locks를 가질 수 없습니다. 또 Shared Mode인 쓰레드가 Shred Mode로 변환 될수 없습니다.
SWR Locks는 Windows Server 2008,Vista 에서만 사용이 가능합니다.
아래는 SRW lock 함수들 입니다.
SRW lock function | Description |
AcquireSRWLockExclusive | SRW lock을 exclusive mode로 얻습니다. |
AcquireSRWLockShared | SRW lock을 shared mode로 얻습니다. |
InitializeSRWLock | SRW lock을 초기화 합니다. |
ReleaseSRWLockExclusive | exclusive mode인 SRW lock을 해제 합니다. |
ReleaseSRWLockShared | shared mode인 SRW lock을 해제 합니다. |
SleepConditionVariableSRW | SRW작업이 완료 될때까지 Sleep 합니다. |
커널 모드
유저 모드에서 커널 오브젝트에 접근을 할 때 시스템은 커널 모드로 변환을 합니다.
커널 오브젝트에는 File, Event, Mutex, Semaphore, Process, Waitable Timer, Job, Thread 가 있습니다.
커널 모드에서 동기화는 커널 오브젝트가 non-Signal인지 Signal 상태인지를 보고 쓰레드를 스케쥴링합니다.
커널 오브젝트가 signal상태이면 쓰레드가 돌아갈 준비가 된 상태이고, non-signal 상태이면 쓰레드는 기다림(Wait)상태 입니다.
이벤트(EVENT)
가장 많이 보편화되고 많이 쓰는 다중 쓰레드 동기화 오브젝트가 이벤트가 아닌가 생각이 됩니다.
이벤트로 다중 쓰레드 동기화 하는 방법에는 두가지 방법이 있습니다.
개발자가 직접 이벤트를 수동으로 signal/non-signal 상태로 변환 하는 것과 시스템이 자동으로 이벤트를 signal/non-signal상태로 변환 하는 방법입니다.
이벤트를 기다리는 방법은 WaitForSingleObjec/WaitForSingleObject함수를 사용합니다. 이벤트가 non-signal상태가 될 때까지 쓰레드는Wait상태로 됩니다.
이벤트 및 모든 커널 오브젝트는 사용이 끝나면 CloseHandle로 사용을 종료 해야 합니다.
Event function | Description |
CreateEvent | 이벤트 오브젝트를 생성하거나 오픈 합니다. |
CreateEventEx | 이벤트 오브젝트를 생성하거나 오픈 합니다.(접근 권한을 줄수 있습니다.) |
OpenEvent | 존재하는 이름이 있는 이벤트 오브젝트를 오픈 합니다. |
PulseEvent | 특정 이벤트 오브젝트를 signal 상태로 바꾸고 일정시간 후 non-signal상태로 바끕니다. |
ResetEvent | 이벤트 오브젝트를 non-signal 상태로 놓습니다. |
SetEvent | 이벤트 오브젝트를 signal 상태로 놓습니다. |
간단 하게 SDI 프로그램에서 수동 모드(passive mode) 이벤트를 이용하여 사각형과 원을 그리는 멀트 쓰레드 프로그램을 보겠습니다.
BOOL CEventSampleView::PreCreateWindow(CREATESTRUCT& cs) { // TODO: CREATESTRUCT cs를수정하여여기에서 // Window 클래스또는스타일을수정합니다. srand( (unsigned)time( NULL ) ); //종료조건을초기화합니다. m_bContinue = TRUE; //수동모드이벤트오브젝트를생성합니다. m_DrawEvent = CreateEvent(0, TRUE, TRUE, _T("DrawEvent")); //시그널상태로둡니다. SetEvent(m_DrawEvent); return CView::PreCreateWindow(cs); } /*OnDraw 메시지 핸들러에서 사각형을 그리는 쓰레드와 원을 그리는 쓰레드를 생성합니다.*/ void CEventSampleView::OnDraw(CDC* /*pDC*/) { CEventSampleDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; m_RectThrd = AfxBeginThread(RectThreadProc,reinterpret_cast<LPVOID>(this)); m_CricleThrd = AfxBeginThread(CircleThreadProc,reinterpret_cast<LPVOID>(this)); // TODO: 여기에원시데이터에대한그리기코드를추가합니다. } /*사각형을 그리는 쓰레드 프로시져*/ UINT CEventSampleView::RectThreadProc(__in LPVOID lpParameter) { CEventSampleView* pView = reinterpret_cast<CEventSampleView*>(lpParameter); int x=0,y=0, cx=0, cy=0; cx = 100; cy = 100; while(pView->m_bContinue) { //시그널상태가될때까지기다립니다. WaitForSingleObject(pView->m_DrawEvent, INFINITE); //이벤트오브젝트를받아오면넌시그널상태로둡니다. ResetEvent(pView->m_DrawEvent); HDC hDC = ::GetDC(pView->GetSafeHwnd()); x= rand()%300; y = rand()%300; ::Rectangle(hDC,x, y, x+cx, y+cy); ::ReleaseDC(pView->GetSafeHwnd(),hDC); //작업이끝나면이벤트를시그널상태로두어다음쓰레드가가질수 //있도록합니다. SetEvent(pView->m_DrawEvent); } DWORD dCode=0; GetExitCodeThread(pView->m_CricleThrd->m_hThread, &dCode); AfxEndThread(dCode, TRUE); return 0; } /*원을 그리는 쓰레드 프로시져*/ UINT CEventSampleView::CircleThreadProc(__in LPVOID lpParameter) { CEventSampleView* pView = reinterpret_cast<CEventSampleView*>(lpParameter); int x=0,y=0, cx=0, cy=0; cx = 100; cy = 100; while(pView->m_bContinue) { WaitForSingleObject(pView->m_DrawEvent, INFINITE); ResetEvent(pView->m_DrawEvent); HDC hDC = ::GetDC(pView->GetSafeHwnd()); x= rand()%300; y = rand()%300; ::Ellipse(hDC,x, y, x+cx, y+cy); ::ReleaseDC(pView->GetSafeHwnd(),hDC); SetEvent(pView->m_DrawEvent); } DWORD dCode=0; GetExitCodeThread(pView->m_CricleThrd->m_hThread, &dCode); AfxEndThread(dCode, TRUE); return 0; } //프로그램이 종료할 때 종료조건을 맞춰 주고 쓰레드 종료 메시지를 주어 정상적으로 //종료하도록 하고, 이벤트 핸들을 닫습니다. void CEventSampleView::OnDestroy() { CView::OnDestroy(); m_bContinue = FALSE; DWORD dExitCode = 0; GetExitCodeThread(m_CricleThrd->m_hThread,&dExitCode); PostQuitMessage(dExitCode); GetExitCodeThread(m_RectThrd->m_hThread,&dExitCode); PostQuitMessage(dExitCode); CloseHandle(m_DrawEvent); } |
뮤텍스(MUTEX)
뮤텍스는 하나의 공유자원에 대한 상호 배타(mutual exclusive)적으로 동기화 하는 방법입니다. 뮤텍스는 서로 다른 프로세스의 쓰레드의 동기화를 할수 있습니다. 이 특성을 이용해서 보통 하나 이상의 프로그램을 실행하기 위해 뮤텍스를 이용합니다.(이걸 깨는 방법도 있죠.)
뮤텍스는 다음과 같은 규칙이 있습니다.
Ø 쓰레드의 ID가 0(유효하지 않은 쓰레드 ID)이면 뮤텍스의 소유권은 어느 쓰레드에게도 없다는 의미 이고 뮤텍스 오브젝트는 시그널된 상태입니다.
Ø 쓰레드 ID가 0이 아닌 값이면, 해당쓰레드(생성 시킨 쓰레드)가 소유권을 가지면 뮤텍스 오브젝트는 non-signal상태 입니다.
Ø 다른 커널 오브젝트와는 달리 뮤텍스는 소유권(thread ownership)이라는 개념이 있습니다.
뮤텍스를 해제할 때(ReleaseMutex), 쓰레드 ID와 생성할 때 설정한 쓰레드의 ID가 맞지 않으면 해제에 실패하고 시스템은 해당 뮤텍스의 시그널 상태를 기다리는 다른 쓰레드를 스케쥴링 합니다.
뮤텍스의 소유권을 가진 쓰레드가 뮤텍스를 해제 하지 않고 종료 되면, 시스템은 해당 뮤텍스를 “abandoned”상태로 두고, 이 뮤텍스를 기다리는 쓰레드를 찾아 기다리고 있는 쓰레드에 뮤텍스의 소유권을 주고 해당 쓰레드를 스케쥴링 합니다.
Mutex function | Description |
CreateMutex | 뮤텍스 오브젝트를 생성하거나 오픈 합니다. |
CreateMutexEx | 뮤텍스 오브젝트를 생성하거나 오픈합니다. 접근 권한 속성을 둘 수 있습니다. |
OpenMutex | 이름이 있는 뮤텍스 오브젝트를 오픈 합니다. |
ReleaseMutex | 뮤텍스 오브젝트를 해제합니다. |
http://msdn2.microsoft.com/en-us/library/ms686927(VS.85).aspx
세마포어(SEMAPHORE)
세마포어는 공유 자원의 카운팅의 용도로 사용합니다.
세마포어는 사용 개수(usage count)이외에 signed 32비트 값 2개를 더 가지고 있습니다.
Ø 최대 리소스 카운트(maximum resource count) : 세마포어가 관리할 수 있는 최대 리소스의 개수.
Ø 현재 리소스 카운트(current resource count) : 현재 사용 가능한 리소스의 개수.
세마포어는 다음과 같은 규칙을 가지고 동작을 합니다.
Ø 현재 리소스 카운터가 0보다 크면(>0) 세마포어 오브젝트는 signal상태입니다.
Ø 현재 리소스 카운터가 0이면, 세마포어 오브젝트는 non-signal상태입니다.
Ø 시스템은 현재 리소스카운터를 – 값이 되지 않도록 합니다.
Ø 현재 리소스 카운터는 최대 리소스 카운터 보다 클 수 없습니다.
Semaphore function | Description |
CreateSemaphore | 세마포어를 생성/오픈 합니다. |
CreateSemaphoreEx | 세마포어를 생성/오픈 합니다. 접근 권한을 둘 수 있습니다. |
OpenSemaphore | 이름이 있는 세마포어 오브젝트를 오픈합니다. |
ReleaseSemaphore | 사용 가능한 리소스 개수를 증가 시킵니다.. |
세마포어 사용 예는 아래 사이트를 참조 하세요.
http://msdn2.microsoft.com/en-us/library/ms686946(VS.85).aspx
Waitable Timer
Waitable Timer 오브젝트는 특정 시간이 되면 오브젝트가 시드널 됩니다.
특정 시간 마다 어떤 동작을 해야할 때 사용할 수 있습니다.
Waitable-timer function | Description |
CancelWaitableTimer | Waitable Timer 오브젝트를 비활성화 시킵니다. |
CreateWaitableTimer | Waitable Timer를 생성하거나 오픈 합니다. |
CreateWaitableTimerEx | Waitable Timer를 생성/오픈 합니다. |
OpenWaitableTimer | 이름이 붙여진 Waitable Timer 오브젝트를 오픈 합니다. |
SetWaitableTimer | Waitable Timer 오브젝트를 활성화 시키거나, Waitable Timer 오브젝트가 시그널 되었을 때 완료 통보 프로시져를 등록 할 수 있습니다.. |
TimerAPCProc | SetWaitableTimer 함수로 등로한 완료 통보 프로시져 선언. |
Timer-queue Timer
Timer queue Timer 오브 젝트는 일정시간이 지나면 시그널되는 동작은Waitable Timer 와 같습니다.
Timer Queue Timer 오브젝트는 일정 시간이 지나면 시그널 되는 오브젝트이고, Timer Queue오브젝트가 Timer Queue Timer 오브젝트를 큐 형태로 관리하면서
해당 프로 시져를 호출 합니다.
위의 그림은 Timer Queue오브젝트가 Timer Queue Timer 오브젝트를 관리하고,
Timer Queue Timer가 시그널 되면 해당 프로시져를 호출하는 모습입니다.
Timer-queue timer function | Description |
ChangeTimerQueueTimer | Timer queue timer 오브젝트의 속성을 변경 합니다.. |
CreateTimerQueue | Timer Queue 오브젝트를 생성합니다. |
CreateTimerQueueTimer | Timer Queue Timer 오브젝트를 생성합니다. |
DeleteTimerQueue | 타이머 큐 오브젝트를 삭제 합니다. |
DeleteTimerQueueEx | 타이머 큐 오브젝트를 삭제 합니다. |
DeleteTimerQueueTimer | Timer Queue 에 있는 Timer Queue Timer 오브젝트를 삭제 합니다. |
쓰레드간 통신
마지막으로 쓰레드간 통신 하는 방법을 알아 보겠습니다.
윈도우 프로그래밍을 하면 윈도우에 SendMessage/PostMessage 함수로 메시지를 보내 듯이 쓰레드에 메시지를 보내고 받으면서 쓰레드간 통신을 할 수 있습니다.
쓰레드에 메시지를 보내는 함수는 PostThreadMessage 입니다.
BOOL PostThreadMessage(
DWORD idThread, /*해당 쓰레드 ID*/
UINT Msg, /*메시지 ID*/
WPARAM wParam, /*메시지를 받는 쓰레드로 넘겨주는 WPARAM인자*/
LPARAM lParam /* 메시지를 받는 쓰레드로 넘겨주는 LPARAM인자*/
);
PostThread로 보낸 메시지는 쓰레드 프로시져에서PeekMessage/GetMessage로 받을 수 있습니다.
http://msdn2.microsoft.com/en-us/library/ms644936(VS.85).aspx
BOOL GetMessage(
LPMSG lpMsg, //MSG 구조체의 포인터 타입
HWND hWnd, //윈도우 핸들
UINT wMsgFilterMin, //필터링할 최소 메시지 ID
UINT wMsgFilterMax //필터링할 최대 메시지 ID
);
http://msdn2.microsoft.com/en-us/library/ms644943.aspx
BOOL PeekMessage(
LPMSG lpMsg, //MSG 구조체의 포인터 타입
HWND hWnd, //윈도우 핸들
UINT wMsgFilterMin, //필터링할 최소 메시지 ID
UINT wMsgFilterMax, //필터링할 최대 메시지 ID
UINT wRemoveMsg //메시지 큐에 해당 메시지를 지울지안지울지 설정
);
PeekMessage와 GetMessage의 차이점은, GetMessage는 메시지가 메시지 큐에 들어 올때가지 블록되고 Peek메시지는 메시지가 없으면 FALSE를 리턴 합니다.
더 자세한 사항은 차고 사이트를 참고 하세요.
아래 예제 코드는 쓰레드 프로시져에서 메시지를 확인하고, 다른 작업을 수행 하는 예제 코드입니다.
unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler) { while (bExit == FALSE) { MSG msg; BOOL res = PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); if (res) { //0x404메세지 가 들어오면 특정 작업 수행 if (msg.message = 0x404) { std::cout<<"Message Received"<<std::endl; } } //다른 작업 수행 } DWORD exitCode; GetExitCodeThread(InputThrd, &exitCode); _endthreadex(exitCode); return 0; } …………………………………………. //다른 쓰레드에서 쓰레드에 메시지를 보냅니다. BOOL res = PostThreadMessage(ID1, 0x00404, 0,0 ); |
참고 사이트 및 서적
Windows Via C/C++
http://msdn2.microsoft.com/en-us/library/aa904937(VS.85).aspx
http://windows-programming.suite101.com/article.cfm/win32_message_processing_primer
=======================
=======================
=======================
출처: http://thermidor.tistory.com/23
C++ 동기화 객체(Critical Section, Mutex, Semaphore, Event)
출처: http://thermidor.tistory.com/23 [떼르미의 『IT, 그리고 세상』]
크리티컬 섹션
- 유저레벨의 동기화 방법 중, 유일하게 커널 객체를 사용하지 않음.
- 내부 구조가 단순하여 동기화 처리에 대한 속도가 빠르다.
- 동일한 프로세스내에서만 사용.
- 커널 객체를 사용하지 않기 때문에 핸들을 사용하지 않고, CRITICAL_SECTION라는 타입을
정의하여 사용.void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
크리티컬 섹션을 초기화한다. 여기 들어가는 인자는 여러개의 스레드에 참조가 되야 하므로 주로
전역에서 쓰인다.void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
생성된 크리티컬 섹션을 삭제한다. CRITICAL_SECTION 구조체는 구체적으로 사용할 일이 없다.
그냥 주소를 넘겨주기만 하면 된다.
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
이 사이에서 공유 자원을 안전하게 액세스한다.
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
동기화 대기 함수DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMiliseconds);
hHandle는 동기화 객체를 나타내고 dwMiliseconds는 기다리는 시간을 정한다. 역시 INFINITE로
지정하면 무한대로 기다린다. 반환값은 세가지 종류이다 성공을 하였을때 WAIT_OBJECT_0
hHandle객체가 신호상태가 된경우 WAIT_TIMER 설정된 시간을 경과하였을 경우
WAIT_ABANDONED 포기된 경우.DWORD WaitForMultipleObject(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll,
DWORD dwMiliseconds);
위의 WaitForSingleObject함수가 하나의 객체에 동기화를 기다리는데 비해 이 함수는 복수개의
동기화 객체를 대기할 수 있다. 동기화 객체의 핸들 배열을 만든후 lpHandles인수로 배열의
포인터를 전달해주면 nCount로 배열의 갯수를 넘겨준다. fWaitAll이 TRUE이면 모든 동기화 객체가
신호상태가 될 때까지 대기하며, FALSE이면 그중하나라도 신호상태가 되면 대기상태를
종료한다. 리턴값의 의미가 조금 다르다. WAIT_TIMEOUT은 같고 bWaitAll이 TRUE이면
WAIT_OBJECT_0이 리턴되면 모든 동기화 객체가 신호상태이라는 말이고 FALSE이면
lpHandles배열에서 신호상태가 된 동기화 객체의 인덱스를 넘겨준다.
이경우 lpHandles[리턴값 - WAIT_OBJECT_0]의 방법으로 신호상태가 된 동기화객체의 핸들을
구할수 있다. 뮤텍스 - 최초에 Signaled 상태로 생성되어지며, WaitForSingleObject()와 같은 대기 함수를 호출함으로
써 NonSignaled 상태가 된다.
- 만약 A라는 스레드가 뮤텍스를 소유하고 있고, B라는 스레드가 뮤텍스를 사용하기 위하여 대기
하고 있을 때, A라는 스레드가 잘못된 연산을 수행하거나 강제 종료되어서 소유하고 있던 뮤텍스
를 반환하지 않았을 때에 B라는 스레드는 뮤텍스를 얻기 위해 무한정 기다릴까?
==> 만약 크리티컬 섹션을 사용하였다면 B라는 스레드는 무한정 기다릴게 될 것이다.
하지만, 뮤텍스의 경우는 자신이 소유한 스레드가 누군지 기억하고 있다. 그리고
Windows 운영체제에서 뮤텍스를 반환하지 않는 상태에서 스레드가 종료될 경우
그 뮤텍스를 강제적으로 Signaled상태로 해 준다.
- 만약 같은 스레드가 중복으로 뮤텍스를 호출할 경우 데드락이 발생할까?
==> 발생하지 않는다. 왜나면 같은 스레드가 중복으로 뮤텍스를 호출할 경우는 내부 Count만
증가시키고, 진입은 허용한다. 그리고 나중에 내부 Count가 '0'으로 될 때 Signaled 상태로
해 준다.(이 부분은 크리티컬 섹션도 동일한 개념)
크리티컬 섹션에 비해서 느리다
크리티컬 섹션의 경우 구조체의 값을 통해 잠그기를 허용하는데 비해 뮤텍스는 객체를 생성하기 때문이다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner,
LPCTSTR lpName);
lpMutexAttributes는 보안속성으로 보통 NULL로 지정한다. bInitialOwner은 뮤텍스 생성과 동시에
소유할것인지 지정하는데 TRUE이면 이 스레드가 바로 뮤텍스를 소유하면서 다른 스레드는
소유할수 없다. lpName는 뮤텍스의 이름이다. NULL설정가능. 반환값은 뮤텍스의 핸들이다.HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
뮤텍스를 연다. 프로세스 ID와 마찬가지로 뮤텍스의 이름은 전역적으로 유일하다.BOOL ReleaseMutex(HANDLE hMutex);
해당 스레드의 뮤텍스 소유를 해제하여 다른 스레드가 가질수 있도록 해준다.HRESULT CloseHandle(HANDLE hHandle);
모든 커널 객체와 마찬가지로 생성된 뮤텍스를 파괴할때 사용한다. 반환값은 S_OK면 성공 그외의
값은 에러이다. 포기된 뮤택스 만약 뮤텍스를 소유하고 있는 스레드가 ExitThread나 TerminateThread로
비정상적으로 종료시켰을 경우 강제로 뮤텍스를 신호상태로 만들어준다. 그러므로 대기중인 다른
스레드에서 뮤텍스를 가지게 되는데 WaitForSingleObject함수의 리턴값으로 WAIT_ABANDONED값을
전달받음으로 이 뮤텍스가 정상적인 방법으로 신호상태가 된 것이 아니라 포기된 상태임을 알 수 있다.
중복소유 뮤텍스를 여러번 겹쳐서 사용했을 경우 데드락과 같은 상태에 빠질수도 있을것이다.
하지만 중복으로 소유하기위해 Wait~Objet함수를 호출하여 기다릴때 뮤텍스를 여러번에 겹쳐서
소유하는게 아니라 한번의 뮤텍스를 소유하고 소유횟수 (카운트)를 증가시킨다. 단, 다시 뮤텍스를
신호상태로 만들기 위해서는 ReleaseMutex를 카운트많큼 호출해주어야한다.
중복 소유에 대해서는 크리티컬 섹션과 같다. 공유자원의 해야 할 일 WaitForSingleObject와 같은
대기상태에서 무한정으로 기다릴게 아니라 dwMiliseconds에 0을 넣어주고 반환값
WAIT_TIMEOUT을 체크해주면 if문등을 통해 쉽게 공유자원외의 일을 할 수 있다. 세마포어세마포어와 뮤텍스는 유사한 동기화 객체이다. 뮤텍스는 하나의 공유자원을 보호하는데 비해
세마포어는 일정 개수를 가지는 자원을 보호할수 있다. 여기서 자원이라함은 윈도우, 프로세서,
스레드와 같은 소프트웨어적인거나 어떤 권한과 같은 무형적인것도 포함된다.HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName);
lMaximumCount는 최대 사용 개수 lInitialCount에 초기값을 지정. 아주 특별한 경우외에는 이 두
값이 같다. 세마포어는 뮤텍스와 같이 이름을 가질 수 있고 이름을 알고 있는 프로세스는 언제든지
OpenSemaphore로 핸들을 구할 수 있다. 역시 파괴할때는 CloseHanle함수를 사용한다.HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
뮤텍스 부분과 같다.BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReelaseCount, LPLONG lpPreviousCount);
lReleaseCount로 사용한 자원의 개수를 알려줌. lpPreviousCount는 세마포어 이전 카운트를 리턴받기 위한 참조 인수이다.
NULL가능하다. 이벤트위의 동기화객체들이 공유자원을 보호하기 위해 사용되는 데 비해 이벤트는 스레드의 작업순서나
시기를 조정하기 위해 사용된다.HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
bManualReset은 이벤트가 수동 리셋(스레드가 비신호상태로 만들어줄 때까지 신호상태를 유지)인지
자동 리셋(대기 상태가 종료되면 자동으로 비신호상태가 된다.)인지를 결정한다. TRUE이면 수동이다.
bInitialState가 TRUE이면 자동으로 신호상태로 들어가 이벤트를 기다리는 스레드가 곧바로 실행할 수
있게 한다.HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
다른 부분과 같다.BOOL SetEvent(HANDLE hEvent);
다른 동기화 객체와는 다르게 사용자 임으로 신호상태와 비신호상태를 설정할 수 있다. 위의 함수는
신호상태로 만들어 준다.BOOL ResetEvent(HANDLE hEvent);
비신호 상태로 만든다. 일반적으로 자동리셋을 사용하는데 이벤트를 발생시켜 대기 상태를 풀 때
자동으로 비신호 상태로 만드는 것이다. 하지만 여러개의 스레드를 위해서 이벤트를 사용한다면
문제가 될수도 있다. 그러므로 수동리셋으로 이벤트를 생성후 ResetEvent함수로 수동리셋을 한다. 실험
Interlock | CriticalSection | Mutex | Not sync | |
Real Time | 09:234 | 09:953 | 1:59:734 | 09:000 |
User Time | 18:406 | 18:937 | 35:187 | 17:859 |
Kernel Time | 00:015 | 00:375 | 1:28:608 | 00:000 |
출처: http://thermidor.tistory.com/23 [떼르미의 『IT, 그리고 세상』]
=======================
=======================
=======================
출처: http://cent84.tistory.com/94
프로세스간 동기화가 필요 하여 조사 하였고 그것을 바탕으로하여 씀.
쓰레드를 동기화 하는 방법
1. Mutax
2. Semaphore
3. 크리티컬 섹션
4. WaitforsingleObject 또는 WaitforMultiObject
이중에 제일 쉽게 아주 간단하면서 효과적이고 시스템의 부하도 적은 방법인 WaitforsingleObject 부터 나열하겠습니다.
사용방법
1. 쓰레드 이벤트 핸들을 정의
2. 쓰레드 함수 만듬
3. 이벤트 관련된 부분 만듬
4. 이벤트 발생
먼저 쓰레드의 이벤트 핸들을 정의합니다
이 이벤트 핸들은 쓰레드에서 이 핸들에 이벤트가 발생할경우 WaitForSingleObject()를 통과하기 위해서 필요합니다
HANDLE m_hEvent;
HANDLE m_hThread;
m_hEvent는 이벤트 핸들이고 m_hTread는 쓰레드 핸들입니다
이제 쓰레드 함수를 만듭니다
UINT WaitThread(LPVOID lParam)
{
CWaitForSingleObjectTestDlg *pDlg = (CWaitForSingleObjectTestDlg *)lParam;
AfxMessageBox(_T("Wait"));
WaitForSingleObject(pDlg->m_hEvent, INFINITE);
AfxMessageBox(_T("End"));
return 0;
}
자 이쓰레드는 먼저 Wait라는 메시지 박스를 출력하고 이벤트가 발생하기를 기다리다가 이벤트가 발생하면 End 라는 메시지 박스를 출력하고 쓰레드를 종료할겁니다
자 이제 이벤트와 관련된 부분을 만듭시다
m_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
ResetEvent(m_hEvent);
이 코드는 이벤트를 만들어 m_hEvent에 붙이고 이벤트를 활성화하는 코드입니다 적당한곳에 넣어주시면 됨
나머지는 쓰레드를 시작하는 부분과 쓰레드 시작후 이벤트를 발생시키는 부분입니다
쓰레드 시작은 다 아시겠지만
m_hThread = AfxBeginThread(WaitThread, (LPVOID)this);
ResetEvent(m_hEvent);
해주면 되겠고
이벤트 발생은 SetEvent(m_hEvent);
return Value
WAIT_FAILED | fail, GetLastError로 원인을 알수가 있다 이경우 logic을 빠져나간다 |
WAIT_ABANDONED | 이 경우는 Event Object를 reset 하고 다시WaitForSingleObject()를 호출한다 |
WAIT_OBJECT_0 | 기다리던 Event가 signal 된 경우 |
WATI_TIMEOUT | time-out 된 경우 |
[Sample Code]
DWORD ret;
while( TRUE ) {
ret = WaitForSingleObject( hHandle, INFINITE );
if( ret == WAIT_FAILED )
return 0;
else if( ret == WAIT_ABANDONED ) {
ResetEvent( hHandle );
continue;
}
else if( WAIT_TIMEOUT )
continue;
else {
ResetEvent( hHandle );
// 원하는 작업을 처리한다.
}
}
크리티컬 섹션이란?
- 크리티컬 섹션 오브젝트를 이용해 임계영역 (접근제어가 필요한 영역) 에 하나의스레드만 접근할 수 있도록 하는 동기화 기법
- 유저레벨의 동기화 방법중 유일하게 커널 객체를 사용하지 않음.
- 내부 구조가 단순하여 동기화 처리에 대한 속도가 빠르다.
- 동일한 프로세스내에서만 사용.
- 커널 객체를 사용하지 않기 때문에 핸들을 사용하지 않고 CRITICAL_SECTION이라는 타입을 정의 하여 사용
- 이미 Lock() 상태인 크리티컬 섹션에 대해 다른 쓰레드가 Lock()을 호출한다면 크리티컬 섹션이 Unlock()될때까지 대기하도록 유도하는 구조이다.
따라서 여러개의 스레드가 동기화 하려면 반드시 하나의 크리티컬 섹션을 공유해야한다.
각 스레드마다 별도의 크리티컬 섹션을 사용하는경우 동기화는 불가능 하다.
동작
1. 전역변수 선언
- CCriticalSection criti;
2. Lock, UnLock함수
동시에 구동되는 스레드들에서 동시에 수행되어서는 안될 부분을 감싸는 함수
UINT ThreadFun(LPVOID lPram)
{
while(true)
{
criti.Lock(); // 이벤트 발생시 실행되는 코드
Sleep(1000); // 1초 단위로 스레드 동작 멈춤
criti.UnLock();
}
return 0;
}
뮤텍스
- 두개이상의 스레드가 동시에 공통자원에 접근하지 않도록 하기위해서 만들어진 알고리즘이다.
- 커널 객체중 유일하게 소유권 개념을 가지고 있다.
뮤텍스는 하나의 공유 자원에 대한 상호 배타적으로 동기화 하는 방법이다. 뮤텍스는 서로 다른 프로세스의 쓰레드를 동기화를 할 수 있습니다. 이 특성을 이용해서 보통 하나 이상의 프로그램을 실행하기 위해 뮤텍스를 이용한다.
뮤텍스 규칙
- 스레드의 ID가 0(유효하지 않은 스레드ID )이면 뮤텍스의 소유권은 어느 스레드에게도 없다는 의미이고 뮤텍스 오브젝트는 시그널 된 상태입니다.
- 스레드 ID가 0이 아닌값이면 해당 쓰레드가 소유권을 가지면 뮤텍스 오브젝트는 non-signal상태다.
- 뮤텍스를 해제할때의 ID와 생성할때 설정한 쓰레드의 ID가 맞지 않으면 해제에 실패하고 시스템은 해당 뮤텍스의 시그널 상태를 기다리는 다른 쓰레드를 스케쥴링 한다.
뮤텍스의 소유권을 가진 스레드가 뮤텍스를 해제하지 않고 종료되면 시스템은 해당 뮤텍스를 abandoned 상태로 두고 이 뮤텍스를 기다리는 스레드를 찾아 기다리고 있는 스레드에 뮤텍스의 소유권을 주고 해당 스레드를 스케쥴링 한다.
크리티컬 섹션과 뮤텍스의 차이
- 크리티컬 섹션은 단일 프로세스의 스레드에 대해서만 동작하고 뮤텍스는 여러 프로세스의 스레드에 대해서도 동작한다.
출처: http://cent84.tistory.com/94 [Again]
=======================
=======================
=======================
프로세스(Process) 와 쓰레드 (Thread)
프로세스는 실행 파일이 실행되어 메모리에 적재된 인스턴스입니다. 운영체제는 여러가지 프로세스를 동시에 실행할 수 있는 능력을 갖추고 있습니다. 즉 컴퓨터로 Youtube에서 노래를 들으면서 코딩을 할 수 있습니다.
그런데, 프로세스도 한번에 여러가지 작업을 수행할 수 있습니다. 쓰레드는 운영체제가 CPU 시간을 할당하는 기본 단위인데, 프로세스는 하나 이상의 쓰레드로 구성됩니다.
쓰레드의 장점
- 사용자 대화형 프로그램에서 응답성을 높일 수 있다.
(프로그램이 무슨 일을 하고 있을 때 대기 할 필요없이 다른 일을 진행할 수 있다) - 멀티 프로세스 방식에 비해 멀티 스레드 방식이 자원 공유가 쉽다.
(프로세스끼리 데이터를 교환할 때 IPC;Inter Process Communication을 이용해야 하지만, 쓰레드는 코드 내의 변수를 같이 사용하기만 하면 된다) - 쓰레드를 사용하면 이미 프로세스에 할당된 메모리와 자원을 그대로 사용한다.
(멀티 프로세스는 프로세스를 띄우기 위해 메모리와 자원을 할당하는 작업을 진행해야 한다)
쓰레드의 단점
- 멀티 쓰레드에서 자식 쓰레드가 문제가 생기면 전체 프로세스가 영향을 받게 된다.
(멀티 프로세스는 자식이 문제가 생기면 해당 프로세스만 죽습니다) - 멀티 쓰레드 구조의 소프트웨어는 구현하기가 까다롭다.
(테스트가 어렵고 디버깅 또한 쉽지 않습니다) - 쓰레드가 CPU를 사용하기 위해서는 작업간 전환 (Context Switching) 을 해야 한다.
(자주 작업 간 전환을 하기 되면 성능이 저하된다)
쓰레드의 상태
.NET Framework 의 ThreadState는 다음과 같습니다.
상태 | 설명 |
Unstarted | 쓰레드 객체를 생성한 후 Thread.Start() 메소드가 호출 되기 전의 상태입니다. |
Running | 쓰레드가 시작하여 동작 중인 상태입니다. Unstarted 상태의 쓰레드를 Thread.Start() 메소드를 통해 이 상태로 만들 수 있습니다. |
Suspended | 쓰레드의 일시 중단 상태입니다. 쓰레드를 Thread.Suspend() 메소드를 통해 이 상태로 만들 수 있으며, Suspended 상태인 쓰레드는 Thread.Resume() 메소드를 통해 다시 Running 상태로 만들 수 있습니다. |
WaitSleepJoin | 쓰레드가 블록(Block)된 상태입니다. 쓰레드에 대해 Monitor.Enter(), Thread.Sleep(), Thread.Join() 메소드를 호출하면 이 상태가 됩니다. |
Aborted | 쓰레드가 취소된 상태입니다. Thread.Abort() 메소드를 호출하면 이 상태가 됩니다. Aborted 상태가 된 쓰레드는 다시 Stopped 상태로 전 환되어 완전히 중지됩니다. |
Stopped | 중지된 쓰레드의 상태입니다. Thread.Abort() 메소드를 호출하거나 쓰레드가 실행 중인 메소드가 종료되면 이 상태가 됩니다. |
Background | 쓰레드가 백그라운드로 동작되고 있음을 나타냅니다. Foreground 쓰레드는 하나라도 살아 있는 한 프로세스 가 죽지 않지만, Background는 여러개가 살아 있어도 프로세스가 죽고 사는 것에는 영향을 미치지 않습니다 하지만 프로세스가 죽으면 Background 쓰레드는 모두 죽습니다. Thread.IsBackground 속성에 true 값을 입력하면 쓰레드를 이 상태로 바꿀 수 있습니다. |
쓰레드의 라이프 사이클
이미지 참조: http://acroama.tistory.com/m/post/43
쓰레드 실행 예제를 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
static void DoSomething()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Thread : {0}", i);
Thread.Sleep(250);
}
}
static void ParametersDosomething(object num)
{
for(int i=0; i<(int)num; i++)
{
Console.WriteLine("ParametersThread : {0}", i);
Thread.Sleep(250);
}
}
static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(DoSomething));
thread.Start();
thread.Join(); // thread 가 종료될 때 까지 대기.
Thread thread2 = new Thread(new ParameterizedThreadStart(ParametersDosomething));
thread2.Start(5); // 매개변수를 갖는 쓰레드 실행하는 방법 (object 매개변수만 넘길수 있다)
for(int i=0; i<5; i++)
{
Console.WriteLine("Main : {0}", i);
Thread.Sleep(500);
}
}
}
}
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
쓰레드 종료하기
쓰레드는 스스로 할일을 마치고 종료하는 것이 가장 좋겠지만, 쓰레드를 종료시켜야 할 경우가 있습니다.
Thread.Abort() 메소드로 가능하지만, 이는 쓰레드를 강제로 종료시켜버립니다. 즉, 도중에 작업이 강제로 종료되도 프로세스 자신이나 시스템에 전혀 영향이 없는 작업에 한해 사용하는 것이 좋습니다. 만약, 수행중인 작업이 시스템에 영향이 있을 거라 판단된다면 다음과 같이 쓰레드를 종료시켜야 합니다.
Thread.Interrupt() 메소드는 쓰레드가 Running State를 피해서 WaitJoinSleep State 에 들어갔을 때 ThreadInterruptedException 예외를 던져 쓰레드를 중지시킵니다. 따라서 절대로 중단되면 안되는 작업을 할 때 이렇게 안정성이 보장된 방법을 사용해야합니다.
쓰레드 종료 예제를 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
static void DoSomething()
{
try
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Thread : {0}", i);
Thread.Sleep(250);
}
}
catch(ThreadInterruptedException e)
{
Console.WriteLine(e);
}
finally
{
Console.WriteLine("====Clearing Resource===");
}
}
static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(DoSomething));
thread.Start();
for(int i=0; i<5; i++)
{
Console.WriteLine("Main : {0}", i);
Thread.Sleep(500);
if (i == 0)
thread.Interrupt();
}
}
}
}
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
쓰레드 간의 동기화하기
각 쓰레드들은 여러가지 자원을 공유하는 경우가 많습니다. 쓰레드가 어떤 자원을 사용하고 있는데, 도중에 다른 쓰레드가 이 자원을 사용한다면 문제가 발생할 수 있습니다.
예를 들면 은행에서 돈을 인출해주려고 할때, ATM 기기에서, 휴대폰에서, 인터넷뱅킹으로, 각각 비슷한 시간에 전재산을 인출해달라고 요청한다면 은행이 3번 모두 전재산을 인출시킨다면 문제가 있겠지요.
예제로 자원을 공유하는 상황을 만들어보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
class Account
{
public int money = 1000;
public void withdraw()
{
if (money <= 0)
{
Console.WriteLine("잔액이 모자랍니다.");
}
else
{
money -= 1000;
}
}
}
static void Main(string[] args)
{
Account account = new Account();
Thread ATM = new Thread(new ThreadStart(account.withdraw));
Thread Phone = new Thread(new ThreadStart(account.withdraw));
Thread Internet = new Thread(new ThreadStart(account.withdraw));
Console.WriteLine("ATM");
ATM.Start();
Console.WriteLine("Phone");
Phone.Start();
Console.WriteLine("Internet");
Internet.Start();
}
}
}
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
위의 코드 결과가 잔액이 모자랍니다가 나올수도 있고 안나올수도 있습니다. 동시에 진행되어 세번 무사히 출금이 이루어 질수도 있는 것입니다.
따라서, 쓰레드들이 순서를 갖춰 자원을 사용할 수 있도록 동기화(Synchronization)을 해주어야 합니다. 자원을 한번에 하나의 쓰레드만 사용할 수 있도록 보장해야 합니다.
C# 에서는 쓰레드 간에 동기화하는 도구로 lock 키워드와 Monitor 클래스를 제공합니다.
-lock 키워드로 동기화하기
한번에 한 쓰레드만 사용할 수 있는 크리티컬 섹션(Critical Section)인 코드영역을 만들어 주어야합니다.
C#에서는 lock 키워드로 감싸주기만 하면 크리티컬 섹션으로 바꿀 수 있습니다.
private readonly object thisLock= new object();
public void withdraw()
{
lock(thisLock) // 크리티컬 섹션영역이 됩니다. 한 쓰레드가 이 코드를 실행하면서
{ // lock 블록이 끝나기 전까지 다른 쓰레드는 이 코드를 실행할 수 없습니다.
if (money <= 0)
{
Console.WriteLine("잔액이 모자랍니다.");
}
else
{
money -= 1000;
}
}
}
lock 키워드는 사용하는 것 자체는 쉽습니다. 하지만, 쓰레드들이 lock 키워드를 만나 크리티컬 섹션을 생성하려고 할 때 이미 하나의 쓰레드가 사용 중이면 락을 얻을 수가 없습니다. 즉 계속 대기하는 상황이 벌어집니다. 이렇게, 소프트웨어의 성능이 크게 떨어집니다. 따라서 쓰레드의 동기화를 설계할 때 크리티컬 섹션을 반드시 필요한 곳에만 사용하는 것이 중요합니다.
또, lock 키워드의 매개변수로 사용하는 객체는 참조형이면 어느 것이든 쓸수 있지만, public 키워드 등을 통해 외부 코드에서도 접근할 수 있는 다음 세가지는 절대 사용하지 않기를 권합니다.
- this : 클래스의 인스턴스는 클래스 내부뿐만 아니라 외부에서도 자주 사용됩니다. lock (this)는 좋지 않습니다.
- Type 형식 : typeof 연산자나 object 클래스로부터 물려받은 GetType() 메소드는 코드 어느 곳에서나 특정 형식에 대한 Type객체를 얻을 수 있습니다. lock(typeof(SomeClass)) , lock(obj.GetType()) 은 좋지 않습니다.
- string 형식 : 절대 string 객체로 lock 하지마시기 바랍니다. lock("abc") 는 좋지 않습니다.
-Monitor 클래스로 동기화하기
public void withdraw() { lock(thisLock) { if (money <= 0) { Console.WriteLine("잔액이 모자랍니다."); } else { money -= 1000; } } } |
public void withdraw() { Monitor.Enter(thisLock); try { if (money <= 0) { Console.WriteLine("잔액이 모자랍니다."); } else { money -= 1000; } } finally { Monitor.Exit(thisLock); } } |
위 두가지 방식은 같은 방법입니다. lock 키워드는 Monitor 클래스의 Enter() 와 Exit() 메소드를 바탕으로 구현되어 있습니다.
그럼에도 불구하고 Monitor클래스 방식을 적는 이유는 Monitor.Wait() 메소드와 Monitor.Pulse() 메소드로 더욱 섬세하게 멀티 쓰레드간의 동기화를 가능하게 해줄 수 있습니다.
Wait() 와 Pulse() 메소드는 반드시 lock 블록 안에서 호출해야 합니다. (그렇지 않으면 CLR 이 SynchronizationLockException을 던집니다)
쓰레드가 WaitSleepJoin 상태가 되면, 동기화를 위해 갖고 있던 lock 을 놓고 Waiting Queue 에 입력되고, 다른 쓰레드가 lock을 얻어 작업을 수행하게 됩니다.
Wait() 와 Pulse() 메소드를 호출할 때 일어나는 일은 다음 그림과 같습니다.
원본이미지참조 : http://www.albahari.com/threading/ (편집하였음)
Thread.Sleep() 메소드도 쓰레드를 WaitSleepJoin State 가 될 수 있지만, Monitor.Pulse() 메소드에 의해 깨어날 수 없습니다. 다시 Running State 가 되려면 매개 변수로 입력된 시간이 경과되거나 Interrupt() 메소드 호출에 의해 깨어날 수 있습니다.
반면에 Monitor.Wait() 메소드는 Monitor.Pulse() 메소드가 호출되면 바로 깨어날 수 있습니다. 따라서 멀티 쓰레드 프로그램의 성능 향상을 위해서 Monitor.Wait() 와 Monitor.Pulse() 를 사용합니다.
사용방법은 다음과 같습니다.
1. 클래스 안에 동기화 객체 필드를 선언합니다.
2. 쓰레드를 WaitSleepJoin State로 바꿔 블록시킬 조건 (Wait()를 호출할 조건) 을 결정할 필드를 선언합니다.
3. 쓰레드를 블록시키고 싶은 곳에서는 lock 블록안에서 2번 과정에서 선언한 필드를 검사하여 Monitor.Wait()를 호출합니다.
4. 3번과정에서 선언한 코드는 lockedCount가 true면 해당 쓰레드를 블록시킵니다. 블록된 쓰레드가 깨어나면 lockedCount를 true로 변경합니다. 다른 쓰레드가 이 코드에 접근하면 3번 과정에서 선언했던 블로킹 코드에 걸려 같은 코드를 실행할 수 없습니다.
작업을 마치면 lockedCount의 값을 다시 false로 바꾼 뒤 Monitor.Pulse()를 호출합니다. 그럼 Waiting Queue에 대기하고 있던 다른 쓰레드가 깨어나서 false로 바뀐 lockedCount를 보고 작업을 수행합니다.
Wait()와 Pulse()를 사용한 예제를 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
class Account
{
public int money = 1000;
private readonly object thisLock = new object();
private bool lockedCount= false; // 다른 쓰레드가 공유된 자원을 사용하고 있는지 판별하기 위해 사용됨
public void withdraw()
{
lock (thisLock)
{
while (lockedCount == true) // 다른 쓰레드에 의해 true로 바뀌어있으면 현재 쓰레드를 블록시킵니다.
Monitor.Wait(thisLock); // 다른 쓰레드가 Pulse()를 호출해 줄때 까지는 WaitSleepJoin State 에 남습니다.
lockedCount = true;
if (money <= 0)
{
Console.WriteLine("잔액이 모자랍니다.");
}
else
{
money -= 1000;
}
lockedCount = false; // 다른 쓰레드를 꺠웁니다.
// 깨어난 쓰레드들은 while의 조건검사를 통해 Wait()를 호출할지 코드를 실행할지 결정합니다.
Monitor.Pulse(thisLock);
}
}
}
static void Main(string[] args)
{
Account account = new Account();
Thread ATM = new Thread(new ThreadStart(account.withdraw));
Thread Phone = new Thread(new ThreadStart(account.withdraw));
Thread Internet = new Thread(new ThreadStart(account.withdraw));
Console.WriteLine("ATM");
ATM.Start();
Console.WriteLine("Phone");
Phone.Start();
Console.WriteLine("Internet");
Internet.Start();
}
}
}
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
테스크(Task)
CPU가 발전하면서 클럭을 높이는 방향에는 한계에 다다르자, 하나의 CPU안에 여러개의 코어를 집적하는 방향으로 제품을 향상시키기 시작했습니다. 이러한 하드웨어의 변화에 맞춰 소프트웨어도 변화를 최대로 활용할 수 있는 방법이 등장하고 있습니다. .NET Framework 에는 System.Threading.Tasks 에는 병행성 코드나 비동기 코드의 실행을 돕는 클래스들이 들어 있습니다. (Task 또한 내부적으로 Thread로 구현됩니다)
Task 클래스를 이용하여 비동기(Asynchronous) 코드를 작성할 수 있습니다.
Task<TResult> 클래스는 코드의 비동기 실행 결과를 얻을 수 있습니다.
Task 클래스는 비동기로 수행할 코드를 Action 델리게이트로 주는 반면 Task<TResult> 는 Func 델리게이트로 줍니다.
즉 Task<TResult> 비동기 작업이 끝나면 Task<>.Result 프로퍼티에 값을 반환하게 됩니다.
Task 예제를 보겠습니다.
<p>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
static void ActionMethod()
{
Thread.Sleep(1000);
Console.WriteLine("ActionMethod Call");
}
static int FuncMethod(object a)
{
Thread.Sleep(500);
Console.WriteLine("FuncMethod Call");
return (int)a+5;
}
static void Main(string[] args)
{
Task task = new Task(ActionMethod);
task.Start(); // task는 비동기 호출로 1초후 완료된다.
Console.WriteLine("Main Logic"); //Main Logic 문구가 바로 출력된다.
Task int task2 = new Task int (FuncMethod, (object)10); // 매개변수와 반환값을 가진 메소드 사용방법 == 태그문제 때문에 int 템플릿처리해줘야합니다
task2.Start();
task2.Wait(); // task2가 메소드가 완료될때 까지 대기
Console.WriteLine("{0}", task2.Result); // 반환값 출력
Console.WriteLine("Main Logic2");
task.Wait(); //task의 메소드가 완료될때 까지 대기
}
}
}
</p>
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
결과를 보면 아시겠지만 실행해보면 Task 클래스에 의해 비동기 호출이 이루어짐을 알 수 있습니다.
추가적으로 예제를 작성하면서 알게 된건데,
위와 같은 코드에서 Main의 Sleep() 가 없을 경우 TaskMethod()에서 Sleep() 다음 코드가 출력되지 않습니다.
즉, Main 함수가 종료되면서 비동기 호출했던 TaskMethod 도 미처 다 실행하지 못하고 종료됩니다.
Thread를 만들어 이용할 경우에는 정상적으로 쓰레드가 다 종료되어야 프로그램이 종료되었지만 말이에요.
메소드를 비동기 호출할 때 끝까지 실행하기를 원한다면 Wait() 메소드를 이용하면 됩니다.
여기서 알 수 있는 것은 프로세스가 생성했던 쓰레드가 다 종료된 후에야 프로세스가 정상종료 되지만, Task는 프로세스가 종료되면서 강제종료되는 것 같습니다.
Parallel
Parallel 클래스는 좀더 쉽게 병렬처리를 하고 싶은 메소드를 처리할 수 있게 도와줍니다.
Parallel.For() 메소드는 주어진 델리게이트에 대하여 병렬로 호출합니다. 몇개의 쓰레드를 사용할 지는 내부적으로 판단하여 알아서 최적화하여 결정합니다.
Parallel 예제를 보겠습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CsharpStudy
{
class Program
{
static void ActionMethod(int num)
{
Thread.Sleep(1000);
Console.WriteLine("ActionMethod Call {0}", num);
}
static void Main(string[] args)
{
Parallel.For(0, 100, ActionMethod);
}
}
}
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
결과를 보면 아시겠지만, 함수를 100번 호출하면서 병렬처리를 하기 때문에 순서가 뒤죽박죽이 되었고,
직접 실행해보면 아시겠지만, 10번 호출할때, 100번호출할때 매개변수값을 바꿔보면 처리방식이 조금씩 달라짐이 보입니다.
그리고 함수호출 한번당 1초정도 소요되게 코드를 짰지만, 병렬처리를 하면서 훨씬 빠르게 해당 프로그램이 종료됨을 보실 수 있습니다.
출처: http://nshj.tistory.com/entry/C-기초문법-11-쓰레드Thread와-테스크Task [namespace:: hyunjin]
=======================
=======================
=======================
출처: http://flowerexcel.tistory.com/17
Java와 VC++ 동기화 비교
<소스 1> wait, notify(All), synchronized
int Count = 0;
/////// Main Thread ///////
synchronized(SyncObj) // (1)
{
Count++;
SyncObj.notify(); // (2)
}
////// Sub Thread 1 ~ N //////
while(true)
{
synchronized(SyncObj) // (3)
{
if(Count == 0)
{
SyncObj.wait(); // (4)
}
if(Count == 0) // (5)
{
// (A) ?
}
Count--; // (6)
}
// ....
}
Java의 동기화를 위해서 사용되는 함수로는 wait와 notify(All)가 있다. 특이한 것은 이 함수들은 반드시 synchronized 블록 안에서만 사용해야 한다는 것이다.
만일 synchronized 블록으로 동기화를 하지 않고 wait와 notify(All) 함수를 사용할 경우 런타임에서 java.lang.IllegalMonitorStateException 예외가 발생하게 된다.
따라서 주석(2), (4)처럼 notify와 wait를 소스 코드에 사용할 경우 반드시 주석(1), (3)과 같이 synchronized 블록으로 동기화를 해야만 한다.
사실 더욱 중요하게 확인해야 할 사실이 있는데, 주석(A) 부분이 과연 실행될 수 있느냐 여부이다. 한번 따져보자!
서브 스레드가 주석(4)의 SyncObj.wait()로 대기 상태에 들어가는 경우는 오직 Count가 0일때 뿐이다. 그리고 대기 상태를 깨우는 곳은 오직 주석(2)의 SyncObj.notify() 뿐이다. 그런데 notify를 하기 전에 Count를 1 증가시켰다. 따라서 주석(4)의 SyncObj.wait()에서 대기 상태인 스레드가 깨어날 때는 Count는 0 이상일 것이다. 따라서 바로 아래의 주석(A) 부분은 절대로 도달되지 않을 것이라고 생각할 수 있다.
그러나 실제로는 주석(A) 부분은 언제든지 진입될 수 있다. 그 이유는 synchronized, wait, notify(All)의 동작 방식의 특이성으로 인하여 발생한다. 즉, synchronized, wait, notify(All)를 정확하게 이해하지 못한다면 주석(A) 부분이 왜 진입되는지를 파악할 수 없다는 의미이다.
Java에서 동기화 관련하여 몇 가지 중요한 사실을 알아야만 한다.
1. synchronized 블록에는 오직 하나의 스레드만이 들어갈 수 있다.
2. 서브 스레드가 wait를 호출할 경우 synchronized 블록에서 빠져나가게 된다. 만일 그렇지 않다면
주 스레드도 synchronized 블록으로 들어갈 수 없기 때문이다. 즉, 주 스레드가 notify를 호출할 수
없는 모순이 발생하게 된다.
3. notify(All)에 의해서 깨어난 서브 스레드는 주 스레드가 synchronized 블록을 빠져나가기 전까지
다음 코드로 진행할 수 없다. 즉, wait에서 깨어만 났지, 주 스레드가 synchronized 블록을
나갈 때까지 기다려야만 한다.
4. 주 스레드가 synchronized 블록을 나가서 wait에서 깨어난 서브 스레드가 다음 코드로 진행하는
순간 synchronized 블록에 다시 진입하게 된다.
위의 네 가지 사실을 통해서 Java의 동기화 코드를 VC++의 동기화 코드로 대체할 수 있다. VC++에서는 Critical Section과 Event 객체를 통해서 거의 유사하게 Java의 동기화를 구현할 수 있다.
<그림 1> Java와 VC++ 동기화 대응
그림에서 볼 수 있듯이, synchronized 블록은 Critical Section으로 대체할 수 있다. 연결선 [1], [3]이 대응 관계를 잘 보여주고 있다.
wait와 notify는 Windows의 Auto Reset Event 객체를 사용하면 된다. notify는 wait로 기다리는 스레드 중에서 하나만을 깨우는 것인데, 바로 Auto Reset Event 객체에 SetEvent를 호출하는 것과 그대로 일치하기 때문이다. 물론 같은 의미에서 notifyAll의 경우 Manual Reset Event 객체에 SetEvent를 호출한다고 생각할 수도 있으나, Reset 처리를 추가적으로 해야 한다는 점에서 완전히 대응되기는 어려운 점이 있다.
따라서 notify의 경우 연결선 [3]처럼 SetEvent로 대체될 수 있다.
wait는 무척 중요하다. 단순히 Event 객체를 기다리는 WaitForSingleObject로만 대응되는 것이 아니기 때문이다. 그림에서 연결선 [4]를 잘 살펴보자! wait가 무려 코드 3줄로 대응되는 것을 볼 수 있다. 이미 설명한 것처럼 wait가 호출되는 경우에는 synchronized 블록을 빠져나가면서 대기 상태에 들어가야 하며, 주 스레드가 synchronized 블록을 나가기 전까지 다음 코드로 진행하는 것을 기다려야 하기 때문에 다시 synchronized 블록으로 들어가야만 한다. 그래서 세 줄의 코드로 대응되는 것이다. 각각 [동기 블록 탈출], [대기], [동기 블록 진입] 이다.
이제 이런 구조에 의해서 주석(A) 부분이 어떻게 도달될 수 있는지 확인해보자!
스레드 1이 주석(4)의 wait에 의해서 대기 상태이다. 이 때 주 스레드는 주석(2)의 notify를 호출하게 된다. notify에 의해서 스레드 1이 wait에서 깨어나긴 하였으나 더 이상 진행은 하지 못하는 상태이다. 이제 주 스레드가 synchronized 블록을 빠져나간다.
여기서 중요한 사실은 주 스레드가 synchronized 블록을 빠져나갔다고 해서 스레드 1이 우선 순위를 가지고 synchronized 블록에 재진입하는 것은 아니라는 사실이다. synchronized 블록에 들어가기 위해서 기다리는 수많은 스레드 중 하나일 뿐이라는 사실이다.
새로운 스레드 2가 스레드 1보다 먼저 synchronized 블록으로 들어간다. 이때 Count는 1인 상태이다.
따라서 if(Count == 0) 이라는 조건에 걸리지 않고 주석(6)에 도달해서 Count는 다시 0이 된다.
스레드 2가 synchronized 블록을 빠져나가는 순간에야 비로서 이미 wait에서 깨어나서 기다리던 스레드 1이 다시 synchronized 블록에 재진입한다. 다음 수행할 코드는 주석(5)이다. 그런데 이미 스레드 2에 의해서 Count는 0이 된 상태이다. 따라서 스레드 1은 주석(A) 부분을 실행하게 되는 것이다.
위에서 제시한 소스는 일반적인 스레드 풀의 구조를 나타낸다. N개의 스레드가 대기 상태이며, 주 스레드가 처리할 일이 있을 경우 작업을 할당하는 것이다. 처리할 작업이 있을 경우 Count를 증가시키면, 증가시킨 Count만큼 스레드가 작업을 할당 받고 처리하게 된다. 처리가 완료될 경우 Count를 다시 감소시킨다. 따라서 주석(A) 부분이 실행된다면 제대로 된 스레드 풀이라고 할 수 없다. 즉, 주석(A) 부분은 절대로 도달되어서는 안 되는 것이다.
그렇다면 어떻게 처리해야만 할까? 사실 답은 무척 간단하다. wait를 호출하기 위하여 검사하는 조건 구문을 if에서 반복 조건문인 while로 고치기만 하면 된다. 즉, 수정된 코드는 다음과 같다.
<소스 2> 수정된 Sub Thread
////// Sub Thread 1 ~ N //////
while(true)
{
synchronized(SyncObj)
{
while(Count == 0) // (1) if -> while
{
SyncObj.wait();
}
if(Count == 0)
{
// (A) ?
}
Count--;
}
// ....
}
주석(1)과 같이 if를 while로만 변경하면 모든 문제가 해결된다. 즉, 절대로 주석(A)는 도달되지 못하게 된다. 왜 그럴까? 위의 시나리오를 그대로 적용해보자! 스레드 2가 Count를 0으로 만들어놓는다. 그리고 이미 wait에서 깨어나서 기다리던 스레드 1이 비로서 다음 실행을 위하여 synchronized 블록으로 재진입한다. 다음 실행은 주석(1)의 while 조건식이다. 이미 스레드 2에 의해서 Count는 0이므로 스레드 1은 다시 wait에서 대기하게 되는 것이다. 그래서 Count가 0일 때 절대로 주석(A)에는 도달할 수가 없다.
while문을 쓰게 되면서 주 스레드도 변경할 여지가 생기게 된다. 바로 notify이다. notify를 notifyAll로 변경해도 된다. 일부 블로그에서는 notify와 notifyAll이 동일하다고 잘못된 지식을 전파하지만 천만의 말씀이다. notify는 wait로 대기하는 스레드 중 오직 하나만을 깨우고, notifyAll은 대기하는 모든 스레드를 깨운다. 즉, 분명히 다른 것이다. 그럼에도 while을 쓰게 되면서 notify와 notifyAll은 같은 효과가 나타나게 된다. notifyAll을 하게 되어도 깨어난 모든 스레드가 while 조건에 의해서 wait에 다시 걸리게 되기 때문이다.
최종 정리를 하면 다음과 같다.
1. wait에 들어가기 위한 조건 구문은 if가 아닌 while이나 for 구문을 사용해야 한다.
2. while이나 for 구문을 사용할 경우 notify와 notifyAll은 동일한 효과를 발휘한다.
출처: http://flowerexcel.tistory.com/17 [성남현인의 프로그래밍 원리]
=======================
=======================
=======================
출처: http://egloos.zum.com/judoboyjin/v/9273940
-Event
SetEvet(): 이벤트를 signaled 상태로 설정한다.
ResetEvent(): 이벤트를 non-signaled상태로 설정한다.
PulseEvent(): 한번의 operation으로 셋과 리셋을 수행한다.
블록킹된 스레드는 이벤트가 signaled일때 해제(릴리즈)되어 나온다.
하나의 스레드가 CEvent::Lock으로 블록킹되어 이벤트가 set되기를 기다리고 있다. 다른 스레드가 이벤트를 set하면 기다리던 스레드는 릴리즈된다. 모든 기다리고 있는 스레드들은 이벤트가 set될때 릴리즈 된다.
윈도우즈는 2개의 다른 이벤트를 제공한다.
1.오토리셋 이벤트
블록킹된 스레드가 해제되면, 자동적으로 non-signaled로 리셋된다.
2.수동리셋 이벤트
블록킹된 스레드가 해제되면, 자동적으로 non-signaled로 리셋되지 않는다.
자동으로 할지 수동으로 할지는 다음사항에 따라 결정하는것이 좋다.
- 단지 하나의 스레드가 이벤트로 사용되어 질경우, 자동리셋 이벤트를 사용해라. SetEvent로 기다리고 있는 스레드를 릴리즈시켜라. 릴리즈되는 순간, 이벤트는 자동리셋되기때문에 ResetEvent를 호출할 필요가 없다.
- 둘이상의 스레드가 이벤트로 사용되어질경우, 수동리셋이벤트를 사용하라. PulseEvent로 모든 기다리는 스레드들을 릴리즈 시켜라. 스레드들이 모두 릴리즈된 이후 PulseEvent가 이벤트를 리셋하기에 ResetEvent를 호출할 필요가 없다. SetEvent로 했을 경우에는 모든 스레드가 릴리즈되는 것을 보장하지 못한다.
오로지 PulseEvent만이 모든 스레드들을 릴리즈 시켜준다. PulseEvent는 set,reset를 해줄뿐아니라, 이벤트를 기다리는 모든 스레드들을 릴리즈시키는것을 보장해준다.
CEvent (BOOL bInitiallyOwn = FALSE,
BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
bInitiallyOwn : 이벤트오브젝트가 초기에 signaled,non-sig인지를 결정한다.
bManualReset : 수동인지,자동인지 결정.
lpszName : 이벤트오브젝트 이름.뮤텍스처럼, 이름을 명기해야한다. 이는 다른 프로세스(어플리케이션)에서도 사용되어 지기 위해 같은 이름으로 하여야 한다.같은 프로세스에서 사용하는 것이라면, NULL이어도 된다.
lpsaAttribute : 보안속성. 걍 NULL로 설정.
- 자동리셋의 예
CEvent g_event; //global , Auto-reset,Initially non-signaled
//Thread A
Init(&buffer);
g_event.SetEvent();
//Thread B
g_event.Lock();
Manage(&buffer);
buffer라는 변수를 2개의 스레드가 사용한다. buffer가 먼저 초기화 된 이후 buffer를 사용하게끔 하기위해서 위와 같은 코딩을 한다. B는 buffer가 초기화가 되고 SetEvent가 호출될때까지 Lock에서 기다린다. A는 buffer를 초기화 하고 SetEvent를 호출하여 event signaled로 설정한다.그리하여 B는 릴리즈가 되고 초기화된 버퍼를 가지고 논다.
Lock()의 리턴값이 0이면, TimeOut expired or error occured. 0이 아니면, signaled로 리턴.
Lock의 첫번째 파라미터는 타임아웃시간임. 초기값은 infinite.
- 수동 리셋의 예
CEvent g_event(FALSE,TRUE); //Initially non-signaled, manual-reset
//Thread A
Init(&buffer);
g_event.PulseEvent();
//Thread B
g_event.Lock(); //waiting for signal
//Thread C
g_event.Lock(); //waiting for signal
Event는 또다른 방식으로 사용할 수 있다. 즉, 스레드 B는 단지 A가 어떠한 작업이 완료됬는지에 따라서 기다리지 않고 행동을 결정하고 싶어할 경우이다.
이는 아래와 같은 방법으로 사용가능하다.
CEvent g_event( FALSE, TRUE );//Initally non-signaled, manual-reset
//Thread A
IsCompleted(&buffer);
g_event.SetEvent();
//Thread B
if( ::WaitSingleObject( g_event.m_hObject, 0 ) == WAIT_OBJECT_0 )
{
//when signaled
}
else
{
//when non-signaled
}
-Semaphores
세마포어는 공유된 리소스를 특정갯수의 스레드만 동시에 사용할 수 있도록 한다. 즉, 10개의 스레드가 움직이고 있다. 각기 스레드는 데이타를 모으고 다 채우면, 소켓통신으로 어디론가 보내는 구조이다. 소켓은 최대 3개뿐이다.
이럴경우, 세마포어가 사용된다. 3개 스레드만 작업을 하고 나머지는 기다리게 하는 동기화 작업을 하는 것이다.
세마포어는 리소스 카운트로 동기화를 시킨다. Lock을 했을 경우 리소스 카운트는 감소하고 그반대는 증가한다. 스레드 하나가 Lock함수를 수행할 경우, 리소스 카운트는 감소하여 0이 되면, 그 스레드는 블록된다. 이경우 다른 스레드가 UnLock함수를 수행하여 리소스 카운트를 증가하여 1이 되면, 블록에서 해제된다. 즉, 리소스 카운트가 0일 경우 블록된다.
세마포어는 하나의 프로세스내에서 또는 다른 프로세스에 속하는 스레드들 내에서 동기화 할 경우 쓰여진다.
CSemaphore(
initial resource count
,maximum resource count
,sema name = null
,보안속성 = null )
CSemaphore g_sema(3,3);
g_sema.Lock();
//Access the shared resource.
g_sema.Unlock();
스레드 A가 위의 것을 수행하면, 리소스 카운트는 3->2 로 되고,
스레드 B가 위의 것을 수행하면, 리소스 카운트는 2->1 로 되고,
스레드 C가 위의 것을 수행하면, 리소스 카운트는 1->0 로 되고,
스레드 D가 위의 것을 수행하면, 리소스 카운트는 0이기에 수행되지 않고 Lock에서 블록킹된다.
그리고 스레드 A,B,C중 어느 것 하나가 UnLock함수를 수행하면 리소스 카운트가 0->1로
증가하여 스레드 D의 블록킹은 해제된다. Lock이 수행되었기에 다시 1->0이 된다.
즉 공유된 리소스를 딱 3개의 스레드만 허용한다는 의미이다.
Lock함수는 역시나 타임아웃시간을 설정할수 있다.
리소스 카운트가 0이 아니거나, 타임아웃 걸렸을 경우에나 블록킹에서 해제된다.
UnLock함수는 이전의 리소스 카운트를 알아내고 거기서 1증가시킨다.
LONG lPrevCount;
g_semaphore.Unlock (2, &lPrevCount);
Unlock( lCount, &lPrevCount )
이전 리소스 카운트를 알수 있다.
-The CSingleLock and CMultiLock Classes
이들 클래스들은 CriticalSection,mutexes,CEvent,CSemaphore를 래핑한다. 이들의 용도는 바로 아래와 같다.
1. CSingleLock
CCriticalSection g_cs;
CSingLock lock(&g_cs);
lock.Lock();
...(A)
lock.Unlock();
만약 (A)지점에서 exception이 발생했을 경우, CSingleLock으로 감싸지않은 CCriticalSection은 Unlock을 영원히 수행할수없기에 다른 스레드는 계속해서 블록킹되어 있게된다.
하지만 위에 처럼 래핑을 시켜놓으면,
exception발생시, CSingLock의 소멸자가 수행되고 이안에서 Lock이된 것들을 모두 UnLock으로 풀어준다.
2. CMultiLock
여러종류의 동기화를 함께 사용할때 쓰여진다. 단, Criticalsection 은 CMultilock을 래핑이 안된다.
CMutext g_mutex;
CEvent g_event[2];
CSyncObject* g_pObjects[3] = { &g_mutext,&g_event[0], &g_event[1] };
CMultiLock lock( g_pObjects,3);
lock.Lock(); //모든 스레드 객체가 signaled이 될 경우 릴리즈
or
lock.Lock(INFINITE,FALSE); //셋중 하나만 signaled이 될 경우 릴리즈
CMultiLock::Lock(
timeout
,bIsAll = TRUE
,wakeupmask = 0 )
bIsAll : TRUE=모든 스레드가 signaled될때 릴리즈.
FALSE=어느 하나가 signaled될때 릴리즈
wakeupmask : 임의 특정메시지로 인해 wakeup모드를 설정할 수있다.
Lock의 리턴값의 의미
CMultilock lock( g_pObject, 3 );
DWORD dwResult = lock.Lock(INFINITE,3);
DWORD nIndex = dwResult - WAIT_OBJECT_0;
if( nIndex == 0 )
// The mutex became signaled.
else if (nIndex == 1 )
// The first event became signaled.
else if (nIndex == 2 )
// The second event became signaled.
만약 INFINITE로 설정을 안했을 경우에는 WAIT_OBJECT_O 을 빼기전에 dwResult == WAIT_TIMEOUT 인지를 비교해야 한다.
좀 더 자세한 내용은 CMultilock::Lock 함수를 MSDN에서 있으니 반드시 참고하도록...
-Writing Thread-Safe Classes
MFC클래스들은 클래스레벨에서는 스레드 안전하지만, 오브젝트 레벨에서는 그렇지 못하다. 다시 말하자면, 같은 클래스의 다른 객체들을 두개의 스레드가 접근하는 것은 안전하지만, 같은 객체를 접근하는 것은 그렇지 못하다. 이유는 성능상의 이유로 오브젝트단에서는 적용하지 않았다. 언록된 Criticalsection을 lock시키는 것은 cpu의 수백의 클락사이클을 소비한다.
다음의 예를 보자
CString g_strFileName;
//Thread A
g_strFileName = pszFile;
//Thread B
pDB->TextOut( x,y,g_strFileName );
이럴경우 화면에 출력되는 것은 무엇이 될까? 새값일까?이전값일까? 아마도 아무것도 아닐것이다. CString객체로 집어넣는 과정에서 B가 수행될경우, 일련의과정들을 방해받는다. 그래서 출력값은 짤리거나 에러로 뜰 가능성이 크다. 그래서 CriticalSection 으로 동기화를 해야한다. 예제는 생략
=======================
=======================
=======================
[출처] http://mind444.tistory.com/56
MFC는 2종류의 쓰레드로 구분할 수 있다.
1. user interface threads
메시지 루프가 존재한다. 윈도우를 만들고 이들 윈도우로 보내진 메시지들을 처리한다. 어플리케이션안에 또하나의 어플리케이션(ui-threads)을 만드는것과 비슷하다.일반적으로 별개로 움직이는 다중 윈도우를 만들때 많이 사용되어 진다.
2. worker threads
직접적으로 메시지를 받지 않고 백그라운드에서 동작되기 때문에 윈도우나 메시지루프들이 필요가 없다.
%이 둘간의실질적인 차이는 아직 잘모르겠다. 좀 더 학습하도록
-Creating a Worker Thread
AfxBeginThread함수는 ui-thread,worker thread 둘다 쓰인다. MFC프로그램에서 Win32::CreateThread함수를 사용하지 말아라.
ex)
CWinThread* pThread = AfxBeginThread (ThreadFunc, &threadInfo);
UINT ThreadFunc (LPVOID pParam)
{
UINT nIterations = (UINT) pParam;
for (UINT i=0; i<nIterations; i++);
return 0;
}
CWinThread* AfxBeginThread (AFX_THREADPROC pfnThreadProc,
LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0, DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
nPriority : 쓰레드 우선순위. 쓰레드들 중에서만 상대적인 우선순위를 정할때 사용한다.
nStackSize : 쓰레드의 스택사이즈 라는데 정확히 뭔지..몰라.
dwCreateFlags : 0 일 경우, 이 함수 호출후 바로 쓰레드가 시작되는 것이고,
CREATE_SUSPENDED일 경우, ResumeThread()가 호출되어야지만 시작되는것이다.
lpSecurityAttrs : 몰라도 되.
--Thread Function prototype
콜백함수이기에 static class 멤버 함수이거나 클래스 밖에 선언된 전역함수이어야 한다.
UINT ThreadFunc( LPVOID pParam )
pParam : 32비트값. AfxBeginThread 함수의 파라미터. 이것은 scalar,handle,객체포인터로도 사용되어 질수 있다.
-Creating a UI Thread
CWinThread를 상속받은 클래스를 사용한다.
ex)
class CMyThread : public CWinThread
CMyThread * pThread = AfxBeginThread(RUNTIME_CLASS(CMyThread),
THREAD_PRIORITY_NORMAL,
0, // stack size
CREATE_SUSPENDED);
pThread->초기값 설정.
pThread->ResumeThread();
또는
CMyThread * pThread = new CMyThread();
pThread->초기값 설정.
pThread->CreateThread();
-Suspending and Resuming Threads
SuspendThread를 호출하면 쓰레드는 멈추고, 내부적으로 Suspend count 가 1 증가 한다. 그리고 ResumeThread를 호출하면 Suspend count는 1 줄고 쓰레드는 다시 움직인다.
ResumeThread는 자신이 호출 할 수 없다. 그리고 이들 함수의 리턴값은 쓰레드의 이전 Suspend Count이다.
-Putting Threads to sleep
::Sleep(0)
현재쓰레드를 중지하고 스케쥴러가 동등한 혹은 높은 우선순위를 갖는 다른 쓰레드를 움직이도록 허락해준다. 만약, 다른 동등한 혹은 높은 우선순위의 쓰레드가 wait 상태가 아니라면, 이 함수는 바로 리턴해서 현재쓰레드를 재시작 한다. (::SwitchToThread.(in NT 4.0 ), ::Sleep(0) (in win32 ))
Sleep함수의 시간은 정확하지 않을 수 있다. 이는 여러환경에 지배받기때문이다. 윈도우즈에서는 쓰레드의 suspend 시간을 보장하는 함수는 존재하지 않는다.
-Terminating a Thread
--Worker Thread
call AfxEndThread.
쓰레드 함수내부의 리턴으로 종료.
--UI Thread
call AfxEndThread.
post WM_QUIT.( ::PostQuitMessage )
(쓰레드 종료함수 사용시 주의할것은 쓰레드 내부에 메모리를 동적할당(new,malloc)해놓고 delete를 안해서 메모리 릭이 날 염려가 있다. 그래서 가급적이면 쓰레드 함수 리턴으로 종료하는 것이 낫다.)
위의 함수들을 호출한 후 한번 제대로 종료됬는지 확인해보라
DWORD dwExitCode;
::GetExitCodeThread (pThread->m_hThread, &dwExitCode);
만약 여전히 살아 있다면 dwExitCode = STILL_ACTIVE(0x103) 으로 되어 있을테다.
- CWinThread 개체 자동 삭제하기
AfxBeginThread 로 스레드 생성할 경우,
CWinThread *pThread = AfxBeginThread( , ,, );
위와 같이 CWinThread 개체 포인터를 반환값으로 받는다. 스레드 종료시, 이 개체는 어떻게 되는가?
MFC는 사용자가 따로 delete pThread; 할 필요없게 자동으로 삭제해 준다.
그런데, 여기서 문제가 있다.
스레드가 종료되지 않았다면, 아래구문은 잘못된 곳이 없다.
::GetExitCodeThread( pThread->m_hThread, &dwExitCode );
( 스레드의 현재 상태값을 반환하는 함수 )
하지만, 종료되어 pThread 개체 포인터 역시 자동으로 삭제되었다면, 위의 구문은 에러를 발생할 것인다.
pThread 는 아무것도 가리 키지 않기 때문에..
해결책)
1. m_bAutoDelete = FALSE; 설정.
-- 이는 곧 사용자가 CWinThread 개체를 delete 해주어야 함을 의미한다.
-- 스레드가 중지가 된 상태에서 m_bAutoDelete = FALSE; 를 설정해주어야 한다.
2. Win32::DuplicateHandle 함수를 호출하여 스레드 핸들의 사본을 생성하여 사용.
-- 해당핸들의 참조카운트가 1 -> 2로 증가되어 CloseHandle() 호출시에 다시 2 -> 1 로 감소될뿐 개체는 죽지 않고 남아있다. 물론, 사용자가 명시적으로 CloseHandle() 을 호출해야 한다.
- 다른 스레드 종료하기
1.
//Thread A
nContinue = 1;
CWinThread *pThread = AfxBeginThread( ThreadFunc, &nContinue );
...
...
nContinue = 0 ; // 스레드 B 를 종료해라.
//Thread B
UINT ThreadFunc( LPVOID pParam )
{
int* pContinue = (int*) pParam;
while( *pContinue )
{
....
}
return 0;
}
2. 스레드 A는 스레드 B가 죽을때 까지 무한 기다리도록 하고 싶을 경우,
//Thread A
nContinue = 1;
CWinThread *pThread = AfxBeginThread( ThreadFunc, &nContinue );
HANDLE hThread = pThread->m_hThread; //사본 생성. 스레드B종료시 pThread 없을 경우 대비.
...
...
nContinue = 0 ; // 스레드 B 를 종료해라.
::WaitForSingleObject( hThread, INFINITE );
//Thread B
//1.의 에제와 같음
::WaitForSingleObject 은 hThread 스레드가 종료될때 까지 무한정(INFINITE) 기다리는 함수이다. 반환 값은 아래와 같다.
-- WAIT_OBJECT_0 : 그 개체가 신호를 받았다. 즉, 스레드가 종료되었다.
-- WAIT_TIMEOUT : 스레드는 살아있지만, 시간 만료로 기다리지 않고 반환되었다.
두번째 매개 변수를 0 으로 설정하고 아래와 같이 사용하는 것이 좋다.
if( ::WaitForSingleObject( hThread, 0 ) == WAIT_OBJECT_0 )
//스레드가 더이상 존재치 않는다.
else
//스레드가 여전히 실행중이다.
- ::TerminateThread( hThread, 0 );
다른 스레드를 직접삭제하는 방법은 위의 함수 딱 한가지 존재한다. 어쩔 수 없는 경우에만 사용하도록 .
- symmetric multiprocessing ( 대칭 다중 처리,SMP )
서로 다른 스레드들을 서로 다른 프로세서에 할당해서 동시에 둘이상의 스레드들을 실행한다. 스케쥴러는 다수의 실행 스레드들을 한번에 실행하고 있는 것처럼 만들기 위해 가능한 효율적으로 이들 사이에 cpu시간을 나누는것이 목표이다.
- Thread Syncronization ( 스레드 동기화 )
윈도우즈는 4종류의 동기화 개체를 지원한다.
-- Critical Sections
-- Mutexes
-- Events
-- Semaphores
1. Critical Sections ( 임계영역 )
- 가장 간단한 형식의 동기화 개체
- 배타적 access 보장 ( 접근 직렬화 ? )
- 같은 프로세스에서 실행되는 스레드 동기화 ( 하나의 응용프로그렘내에서만 )
CCriticalSection g_cs; //전역 데이터
//Thread A
g_cs.Lock(); //....(가)
.... (1)
g_cs.Unlock(); // ...(나)
//Thread B
g_cs.Lock();
...
g_cs.Unlock();
스레드 A는 이미 (1) 항목을 수행 중인데, 스레드 B가 A에 접근하여 동시에 (1)에 수행할 경우, 문제가 발생할 수 있다.
메모리를 2개가 동시에 사용..
그러한 것을 방지 하게 위해 ( 직렬화 ) 위의 예제처럼 Lock을 걸어두면(가),
스레드 B는 A가 Unlock() 할 때(나) 까지 (가)에서 멈춰 있다.
그래서 (1)은 하나의 스레드만 허용된다.
단순히 변수하나를 서로 다른 스레드가 사용하고 싶다면,
g_cs.Lock();
nVar++;
g_cs.Unlock();
위와 같이 쓸수 있지만, 아래와 같은 함수를 이용할 수 있다.
::InterlockedIncrement
::InterlockedDecrement
::InterlockedExchage
::InterlockedCompareExchage
::InterlockedExchangedAdd
따라서 위의 예제는 아래와 같이 사용할 수 있다.
::InterlockedIncrement( &nVar );
( 단 nVar 은 32비트 변수 )
2. MUTEX
- 둘이상의 서로 다른 프로세스( 응용프로그램 )에서 실행되는 스레드 동기화 작업
CMutex g_mutex( FALSE, _T("MyMutex"));
...
g_mutex.Lock();
...
g_mutex.Unlock();
CMutex 생성자의 첫번째 매개변수
- 생성시 참근다. TRUE, 잠그지 않는다:FALSE
두번째 매개변수
- 두 프로세스에서 사용하는 동일한 이름. 꼭 같은 이름이어야 동기화 가능.
if( g_mutex.Lock(10000) )
{ //잠금 해제. 외부 다른 스레드가 g_mutex.Unlock() 호출 했다.
....
g_mutex.Unlock();
}
else
{ // 10초간 기다렸다 반환됨.
....
}
* CriticalSection과 Mutex의 차이
다른 스레드가 Unlock() 할때까지 현재 스레드는 기다려야 하는데, 만약 다른 스레드에서 정말 Unlock()함수를 호출할 기미가 안보일 경우 정말 무한정기다릴까??
CriticalSection : 스레드가 무한정 기다린다.
Mutex : 시스템이 뮤텍스를 해제하여 다른 스레드들이 이용하게 해준다.
-Event
SetEvet(): 이벤트를 signaled 상태로 설정한다.
ResetEvent(): 이벤트를 non-signaled상태로 설정한다.
PulseEvent(): 한번의 operation으로 셋과 리셋을 수행한다.
블록킹된 스레드는 이벤트가 signaled일때 해제(릴리즈)되어 나온다.
하나의 스레드가 CEvent::Lock으로 블록킹되어 이벤트가 set되기를 기다리고 있다. 다른 스레드가 이벤트를 set하면 기다리던 스레드는 릴리즈된다. 모든 기다리고 있는 스레드들은 이벤트가 set될때 릴리즈 된다.
윈도우즈는 2개의 다른 이벤트를 제공한다.
1.오토리셋 이벤트
블록킹된 스레드가 해제되면, 자동적으로 non-signaled로 리셋된다.
2.수동리셋 이벤트
블록킹된 스레드가 해제되면, 자동적으로 non-signaled로 리셋되지 않는다.
자동으로 할지 수동으로 할지는 다음사항에 따라 결정하는것이 좋다.
- 단지 하나의 스레드가 이벤트로 사용되어 질경우, 자동리셋 이벤트를 사용해라. SetEvent로 기다리고 있는 스레드를 릴리즈시켜라. 릴리즈되는 순간, 이벤트는 자동리셋되기때문에 ResetEvent를 호출할 필요가 없다.
- 둘이상의 스레드가 이벤트로 사용되어질경우, 수동리셋이벤트를 사용하라. PulseEvent로 모든 기다리는 스레드들을 릴리즈 시켜라. 스레드들이 모두 릴리즈된 이후 PulseEvent가 이벤트를 리셋하기에 ResetEvent를 호출할 필요가 없다. SetEvent로 했을 경우에는 모든 스레드가 릴리즈되는 것을 보장하지 못한다.
오로지 PulseEvent만이 모든 스레드들을 릴리즈 시켜준다. PulseEvent는 set,reset를 해줄뿐아니라, 이벤트를 기다리는 모든 스레드들을 릴리즈시키는것을 보장해준다.
CEvent (BOOL bInitiallyOwn = FALSE,
BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
bInitiallyOwn : 이벤트오브젝트가 초기에 signaled,non-sig인지를 결정한다.
bManualReset : 수동인지,자동인지 결정.
lpszName : 이벤트오브젝트 이름.뮤텍스처럼, 이름을 명기해야한다. 이는 다른 프로세스(어플리케이션)에서도 사용되어 지기 위해 같은 이름으로 하여야 한다.같은 프로세스에서 사용하는 것이라면, NULL이어도 된다.
lpsaAttribute : 보안속성. 걍 NULL로 설정.
- 자동리셋의 예
CEvent g_event; //global , Auto-reset,Initially non-signaled
//Thread A
Init(&buffer);
g_event.SetEvent();
//Thread B
g_event.Lock();
Manage(&buffer);
buffer라는 변수를 2개의 스레드가 사용한다. buffer가 먼저 초기화 된 이후 buffer를 사용하게끔 하기위해서 위와 같은 코딩을 한다. B는 buffer가 초기화가 되고 SetEvent가 호출될때까지 Lock에서 기다린다. A는 buffer를 초기화 하고 SetEvent를 호출하여 event signaled로 설정한다.그리하여 B는 릴리즈가 되고 초기화된 버퍼를 가지고 논다.
Lock()의 리턴값이 0이면, TimeOut expired or error occured. 0이 아니면, signaled로 리턴.
Lock의 첫번째 파라미터는 타임아웃시간임. 초기값은 infinite.
- 수동 리셋의 예
CEvent g_event(FALSE,TRUE); //Initially non-signaled, manual-reset
//Thread A
Init(&buffer);
g_event.PulseEvent();
//Thread B
g_event.Lock(); //waiting for signal
//Thread C
g_event.Lock(); //waiting for signal
Event는 또다른 방식으로 사용할 수 있다. 즉, 스레드 B는 단지 A가 어떠한 작업이 완료됬는지에 따라서 기다리지 않고 행동을 결정하고 싶어할 경우이다.
이는 아래와 같은 방법으로 사용가능하다.
CEvent g_event( FALSE, TRUE );//Initally non-signaled, manual-reset
//Thread A
IsCompleted(&buffer);
g_event.SetEvent();
//Thread B
if( ::WaitSingleObject( g_event.m_hObject, 0 ) == WAIT_OBJECT_0 )
{
//when signaled
}
else
{
//when non-signaled
}
-Semaphores
세마포어는 공유된 리소스를 특정갯수의 스레드만 동시에 사용할 수 있도록 한다. 즉, 10개의 스레드가 움직이고 있다. 각기 스레드는 데이타를 모으고 다 채우면, 소켓통신으로 어디론가 보내는 구조이다. 소켓은 최대 3개뿐이다.
이럴경우, 세마포어가 사용된다. 3개 스레드만 작업을 하고 나머지는 기다리게 하는 동기화 작업을 하는 것이다.
세마포어는 리소스 카운트로 동기화를 시킨다. Lock을 했을 경우 리소스 카운트는 감소하고 그반대는 증가한다. 스레드 하나가 Lock함수를 수행할 경우, 리소스 카운트는 감소하여 0이 되면, 그 스레드는 블록된다. 이경우 다른 스레드가 UnLock함수를 수행하여 리소스 카운트를 증가하여 1이 되면, 블록에서 해제된다. 즉, 리소스 카운트가 0일 경우 블록된다.
세마포어는 하나의 프로세스내에서 또는 다른 프로세스에 속하는 스레드들 내에서 동기화 할 경우 쓰여진다.
CSemaphore(
initial resource count
,maximum resource count
,sema name = null
,보안속성 = null )
CSemaphore g_sema(3,3);
g_sema.Lock();
//Access the shared resource.
g_sema.Unlock();
스레드 A가 위의 것을 수행하면, 리소스 카운트는 3->2 로 되고,
스레드 B가 위의 것을 수행하면, 리소스 카운트는 2->1 로 되고,
스레드 C가 위의 것을 수행하면, 리소스 카운트는 1->0 로 되고,
스레드 D가 위의 것을 수행하면, 리소스 카운트는 0이기에 수행되지 않고 Lock에서 블록킹된다.
그리고 스레드 A,B,C중 어느 것 하나가 UnLock함수를 수행하면 리소스 카운트가 0->1로
증가하여 스레드 D의 블록킹은 해제된다. Lock이 수행되었기에 다시 1->0이 된다.
즉 공유된 리소스를 딱 3개의 스레드만 허용한다는 의미이다.
Lock함수는 역시나 타임아웃시간을 설정할수 있다.
리소스 카운트가 0이 아니거나, 타임아웃 걸렸을 경우에나 블록킹에서 해제된다.
UnLock함수는 이전의 리소스 카운트를 알아내고 거기서 1증가시킨다.
LONG lPrevCount;
g_semaphore.Unlock (2, &lPrevCount);
Unlock( lCount, &lPrevCount )
이전 리소스 카운트를 알수 있다.
-The CSingleLock and CMultiLock Classes
이들 클래스들은 CriticalSection,mutexes,CEvent,CSemaphore를 래핑한다. 이들의 용도는 바로 아래와 같다.
1. CSingleLock
CCriticalSection g_cs;
CSingLock lock(&g_cs);
lock.Lock();
...(A)
lock.Unlock();
만약 (A)지점에서 exception이 발생했을 경우, CSingleLock으로 감싸지않은 CCriticalSection은 Unlock을 영원히 수행할수없기에 다른 스레드는 계속해서 블록킹되어 있게된다.
하지만 위에 처럼 래핑을 시켜놓으면,
exception발생시, CSingLock의 소멸자가 수행되고 이안에서 Lock이된 것들을 모두 UnLock으로 풀어준다.
2. CMultiLock
여러종류의 동기화를 함께 사용할때 쓰여진다. 단, Criticalsection 은 CMultilock을 래핑이 안된다.
CMutext g_mutex;
CEvent g_event[2];
CSyncObject* g_pObjects[3] = { &g_mutext,&g_event[0], &g_event[1] };
CMultiLock lock( g_pObjects,3);
lock.Lock(); //모든 스레드 객체가 signaled이 될 경우 릴리즈
or
lock.Lock(INFINITE,FALSE); //셋중 하나만 signaled이 될 경우 릴리즈
CMultiLock::Lock(
timeout
,bIsAll = TRUE
,wakeupmask = 0 )
bIsAll : TRUE=모든 스레드가 signaled될때 릴리즈.
FALSE=어느 하나가 signaled될때 릴리즈
wakeupmask : 임의 특정메시지로 인해 wakeup모드를 설정할 수있다.
Lock의 리턴값의 의미
CMultilock lock( g_pObject, 3 );
DWORD dwResult = lock.Lock(INFINITE,3);
DWORD nIndex = dwResult - WAIT_OBJECT_0;
if( nIndex == 0 )
// The mutex became signaled.
else if (nIndex == 1 )
// The first event became signaled.
else if (nIndex == 2 )
// The second event became signaled.
만약 INFINITE로 설정을 안했을 경우에는 WAIT_OBJECT_O 을 빼기전에 dwResult == WAIT_TIMEOUT 인지를 비교해야 한다.
좀 더 자세한 내용은 CMultilock::Lock 함수를 MSDN에서 있으니 반드시 참고하도록...
-Writing Thread-Safe Classes
MFC클래스들은 클래스레벨에서는 스레드 안전하지만, 오브젝트 레벨에서는 그렇지 못하다. 다시 말하자면, 같은 클래스의 다른 객체들을 두개의 스레드가 접근하는 것은 안전하지만, 같은 객체를 접근하는 것은 그렇지 못하다. 이유는 성능상의 이유로 오브젝트단에서는 적용하지 않았다. 언록된 Criticalsection을 lock시키는 것은 cpu의 수백의 클락사이클을 소비한다.
다음의 예를 보자
CString g_strFileName;
//Thread A
g_strFileName = pszFile;
//Thread B
pDB->TextOut( x,y,g_strFileName );
이럴경우 화면에 출력되는 것은 무엇이 될까? 새값일까?이전값일까? 아마도 아무것도 아닐것이다. CString객체로 집어넣는 과정에서 B가 수행될경우, 일련의과정들을 방해받는다. 그래서 출력값은 짤리거나 에러로 뜰 가능성이 크다. 그래서 CriticalSection 으로 동기화를 해야한다. 예제는 생략
=======================
=======================
=======================
출처: http://blog.naver.com/PostView.nhn?blogId=dolicom&logNo=10132602935
미리보기 이벤트(CreateEvent)http://blog.naver.com/dolicom/10132583704Read Thread 예 OVERLAPPED ovRead; OVERLAPPED ovWrite; char gRxBuff[1024]; void threadComm(HANDLE hCom) { OVERLAPPED ov; ZeroMemory(&ov, sizeof ov); ov.hEvent = CreateEvent(0, TRUE, FALSE, 0); SetCommMask(hCom, EV_RXCHAR); DWORD event; DWORD readByte; DWORD dwError; COMSTAT comState; DWORD dwRxByte; while(1) { event = 0; WaitCommEvent(hCom, &event, 0); if ((event & EV_RXCHAR) == EV_RXCHAR) { ClearCommError( hCom, &dwError, &comState); if (dwRxByte = comState.cbInQue) { ReadFile(hCom, gRxBuff, dwRxByte , &readByte, &ovRead) ; //SendMessage( hWnd, WM_COMMAND, readByte, gRxBuff); } } } CloseHandle(ov.hEvent); } HANDLE OpenCom() { HANDLE hCom; hCom = CreateFile("COM1", GENERIC_READ|GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, 0); SetCommMask(hCom, EV_RXCHAR); SetupComm(hCom, 4096, 4096); PurgeComm(hCom, PURGE_TXABORT | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_RXCLEAR); COMMTIMEOUTS timeouts; timeouts.ReadIntervalTimeout = 0xFFFFFFFF; timeouts.ReadTotalTimeoutMultiplier = 0; timeouts.ReadTotalTimeoutConstant = 0; timeouts.WriteTotalTimeoutMultiplier = 1; timeouts.WriteTotalTimeoutConstant = 0; SetCommTimeouts(hCom, &timeouts); DCB dcb; dcb.DCBlength = sizeof(DCB); GetCommState(hCom, &dcb); dcb.BaudRate = 19200; dcb.ByteSize = 8; dcb.Parity = 2; dcb.StopBits = 0; SetCommState(hCom, &dcb); return hCom; } DWORD ComWirte(char *str, int length) { DWORD written; WriteFile(hCom, str, length, &written, &ovWrite); return written; } int CloseCom(HANDLE hCom) { SetCommMask(hCom, 0); PurgeComm(hCom, PURGE_TXABORT | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_RXCLEAR); CloseHandle(hCom); } int main(int argc, char* argv[]) { ovWrite.Offset = 0; ovWrite.OffsetHigh = 0; ovWrite.hEvent = CreateEvent(0, 1, 0, 0); ovRead.Offset = 0; ovRead.OffsetHigh = 0; ovRead.hEvent = CreateEvent(0, 1, 0, 0); HANDLE hCom = OpenCom(); DWORD threadID; CreateThread(0, 0, (LPTHREAD_START_ROUTINE)threadComm, (LPVOID)hCom, 0, &threadID); char txBuff[100] = "Hello"; ComWirte(txBuff, strlen(txBuff) ); CloseCom(hCom); return 0; } 에제2 http://www.windows-api.com/microsoft/VC-MFC/33316032/evrxchar-event-received-but-readfile-sometimes-failed-to-read-a----byte-from-serial-port.aspx 쓰레드 동기화 쓰레드 동기화 에는 크게 4종류에 쓰레드가 있음.. 크리티컬 섹션 ( criticalsection ) 뮤덱스 ( mutex ) 세마포어 ( semaphore ) 위에서 언급한 이벤트 ( event )
criticalsection http://www.nicklib.com/bbs/board.php?bo_table=bbs_lecture&wr_id=52&page=2 크리티컬 섹션을 해석하면 "임계영역"이라고 합니다. 책에서 그렇다는 것이고 저보고 해석하라고 하면 "위험영역"이라고 하고 싶군요. 즉, Source코드 중에 쓰레드에 의해 서로 간섭이 일어나 문제가 일어날 소지가 있는 코드부분을 위험 영역이라고 정하고 이들 구간은 하나의 쓰레드만이 실행하도록 하는 것입니다. 크리티컬 섹션에 사용하는 함수는 총 4가지 VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); VOID EnterCriticalSection(LPCRITICAL_SECTION lpCrtiticalSection); VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); #include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; CRITICAL_SECTION crit; void Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++) { EnterCriticalSection(&crit); tmp=cnt; Sleep(1); cnt=tmp+1; LeaveCriticalSection(&crit); } printf("Thread1 End\n"); _endthread(); } void Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++) { EnterCriticalSection(&crit); tmp=cnt; Sleep(1); cnt=tmp+1; LeaveCriticalSection(&crit); } printf("Thread2 End\n"); _endthread(); } int main(int argc, char *argv[]) { InitializeCriticalSection(&crit); _beginthread(Thread1, 0, NULL); _beginthread(Thread2, 0, NULL); Sleep(5000); printf("%d\n", cnt); DeleteCriticalSection(&crit); return 0; } InterLock http://www.nicklib.com/bbs/board.php?bo_table=bbs_lecture&wr_id=52&page=2 공유된 변수의 값을 변경하는 경우 CPU는 이것을 한번에 처리하지 않습니다. 즉, cnt++; 라는 구문을 실행하면 CPU는 레지스트리에서 cnt의 현재 값을 빼와서 1을 더한 다음에 다시 레지시트리에 넣는 식으로 되어 있습니다. 이렇다 보니까 쓰레드에의해 실행될 경우 레지스트리에서 값을 빼온 후 변경한 값을 다시 집어 넣기 전에 다른 쓰레드로 스위칭될 수 있습니다. 이렇다 보면 다른 쓰레드에서 이 값을 변경하고 다시 지금의 쓰레드로 돌아 왔을 경우 다른 쓰레드에서 변경한 값이 없어질 수 있는 것이죠. 이런 것을 변수에 값을 대입하는 연산은 원자성(Atomicity)이 없다고 이야기합니다. 아시다 시피 원자는 더 이상 쪼갤 수 없는 것이죠? 원자성이 없다는 이야기는 더 쪼갤 수 있다는 의미가 됩니다. 쪼갤 수 있다는 것은 하나의 연산을 처리하는 중 다른 쓰레드로 변환이 될 수 있다는 이야기죠. 인터락함수 LONG InterlockedIncrement(LONG volatile* Addend); LONG InterlockedDecrement(LONG volatile* Addend); LONG InterlockedExchange(LONG volatile* Target, LONG Value); LONG InterlockedExchangeAdd(LONG volatile* Addend, LONG Value); PVOID InterlockedExchangePointer(PVOID volatile* Target, PVOID Value); LONG InterlockedCompareExchange(LONG volatile* Destination, LONG Exchange, LONG Comperand); PVOID InterlockedCompareExchangePointer(PVOID volatile* Destination, PVOID Exchange, PVOID Comperand); 예 #include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; void Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ InterlockedIncrement(&cnt); } printf("Thread1 End\n"); _endthread(); } void Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ InterlockedIncrement(&cnt); } printf("Thread2 End\n"); _endthread(); } int main(int argc, char *argv[]) { _beginthread(Thread1, 0, NULL); _beginthread(Thread2, 0, NULL); Sleep(5000); printf("%d\n", cnt); return 0; } InterlockedExchange InterlockedExchange - 대입 value=exchange InterlockedExchangeAdd - 덧셈 value+=arg InterlockedIncrement - 증가 value++ InterlockedDecrement - 감소 value-- InterlockedCompareExchange - 비교후 바꾸기 if value==check then value=exchange InterlockedExchange The InterlockedExchange routine sets an integer variable to a given value as an atomic operation. <h4>Parameters</h4>TargetPointer to a variable to be set to the supplied Value as an atomic operation.ValueSpecifies the value to which the variable will be set.Return Value InterlockedExchange returns the value of the variable at Target when the call occurred. Comments InterlockedExchange should be used instead of ExInterlockedExchangeUlong, because it is both faster and more efficient. InterlockedExchange is implemented inline by the compiler when appropriate and possible. It does not require a spin lock and can therefore be safely used on pageable data. A call to InterlockedExchange routine is atomic only with respect to other InterlockedXxx calls. Interlocked operations cannot be used on non-cached memory. Requirements IRQL: Any level Headers: Declared in Wdm.h. Include Wdm.h, Ntddk.h, or Ntifs.h. http://www.codemaestro.com/reviews/8#comment-2497 implementation is: The LOCK prefix functions only with the following instructions: 출처 : http://a.tk.co.kr/177 InterlockedExchange는 스레드(Thread)에 안전하게 32bit( INT, LONG, DWORD )형 변수를 교환하는 함수입니다. Visual C++ 6.0 에서는 time_t 자료 형이 4 바이트이지만 Visual Studio 2005에서는 8바이트로 변경되었습니다. 따라서 InterlockedExchange에서는 time_t 자료 형의 변수를 사용하면 안됩니다. http://sokkuma.egloos.com/337730 InterlockedExchange 이 넘이... 무한루프에 빠져 결국은 서비스 자체를 죽일 수 있다는 것을 발견하였당... |
=======================
=======================
=======================
출처: http://www.gamecodi.com/board/zboard.php?id=GAMECODI_Talkdev&no=1846
8개의 스레드를 돌려 Queue 에 들어간 데이터를 처리하는 클래스가 있습니다. 즉 8개의 스레드 각각은 Queue를 Pop() 하여 데이터를 처리하고, 이때 CriticalSection을 이용하여 Queue에 대한 동기화 처리를 해줍니다. (8개의 스레드가 동시에 접근할 가능성이 있으므로) 쉽게 하기 위해 이 스레드를 PopThread라고 합니다. 다른 1개의 스레드는 같은 Queue에 처리할 데이터를 Push()합니다. 물론 이때도 Push()할 때에는 CriticalSection으로 Queue에 대한 동기화를 해줍니다. (위의 8개의 스레드가 동시에 접근하고자 하므로) 이 스레드는 PushThread 라고 합시다. 이때 이 Push()하는 스레드가 처리할 데이터를 Push()했음을 8개의 Pop() 스레드에게 알리기 위해, 즉 PushThread가 데이터를 Push() 했음을 PopThread에게 알리기 위해 Queue에 데이터를 Push() 한 뒤 SetEvent() 를 날려 이를 알립니다. 그리고 PopThread에서는 WaitForSingleObject() 로 PushThread에서 SetEvent 될 이벤트를 무한정 기다립니다. 그런데 여기서 문제가 생깁니다. PushThread 에서 SetEvent를 할때 ? 와 같이, 크리티컬 섹션 동기화는 오직 Queue에만 해주는 경우와 (논리적으로는 이게 맞죠. 크리티컬 섹션을 사용하는 목적 자체가 Queue에 대한 접근을 제어하기 위함이니까요) (이 경우를 A 경우라고 합시다) ? 와 같이 크리티컬 섹션 동기화를 SetEvent()에도 같이 해주는 경우가 있습니다. 다만 이 경우는 논리적으로 약간 말이 안될수도 있는게, 크리티컬 섹션은 Queue에 대한 동기화 처리를 위한 것이지, m_ProcessDataEvent에 대한 동기화 처리를 위한 것은 아니기 때문이죠. 또한 SetEvent는 ThreadSafe 하다고 알고 있습니다. (이 경우를 B 경우라고 합시다) 제 생각으로는 이 두 경우 결과가 같아야합니다. 왜냐하면 SetEvent는 ThreadSafe 하기 때문에 크리티컬 섹션으로 동기화 처리를 해주던 말던 어차피 동기화가 되기 때문이죠. 그런데 실제 테스트에서는 이 두 경우가 다르게 나옵니다. A의 경우, 논리적으로는 문제가 없음에도 불구하고 SetEvent() 되는 이벤트를 기다리는 8개의 스레드가 무한 루프에 빠집니다. WaitForSingleObject() 에서 기다리는 Event가 Set되지 않았음을 뜻하죠( 이 부분은 로그를 통해 확인해본 사항입니다) 반면 B의 경우는 논리적으로 생각하기에는 약간 말이 안되지만, 실제로는 잘 동작합니다. SetEvent도 제대로 동작하고 WaitForSingleObject()역시 무한루프에 빠지지 않습니다. 제 짧은 지식으로는 문제가 없어 보이는 두 경우가 왜 다르게 나오는 걸까요? 혹시 Critical Section과 Event 동기화를 동시에 사용하는 경우에 주의해야 하는 사항이라도 있는 걸까요? Event를 이용하는데 있어서 제가 모르는 버그라도 있는걸까요? ( 작업환경은 VS.2005 입니다 ) 저랑 비슷한 상황을 겪어보셨거나, 답을 알고 계신 분은 한마디 조언 부탁드립니다. ㅠㅠ --------------------------------------------------------------------------------------------------------------------------------------------------------- |
=======================
=======================
=======================
출처: http://pastime0.tistory.com/entry/CreateMutex
쓰레드(Thread)는 멀티프로세스가 지원되는 OS에서 실행의 최소 단위입니다..
쓰레드가 모여서 하나의 프로세스를 구성하지요... 그러니까 프로세스를 기동시키면
한개 이상의 쓰레드가 기동되는것을 의미하는것죠....
하나의 프로세스에 여러개의 쓰레드를 기동시키기도 합니다..
프로세스는 말 그데로.. 독립적인 실행의 단위입니다...
대충 프로세스와 쓰레드는 설명이 되었고..
Visual C++을 사용하면 동기화에 관련된 몇가지 모듈을 제공합니다..
크리티컬섹션(CriticalSection)
뮤텍스(Mutex)
세마포어(Semaphore)
그 밖에 기타 Event, Overlapped 등을 지원합니다...
동기화(상호배제)에 관해서는 님께서 대충 아신다고 하니.. 그 부분은 설명하지 않고..
크리티컬섹션과 뮤텍스의 차이점과 CreateMutex()에 관해서만 설명드리겠습니다..
크리티컬섹션이나 뮤텍스는 어떻게 보면 기능상 똑같다고 생각하시면 됩니다. 약간은 차이는 나지만..
근본적인 차이는 크리티컬섹션은 하나의 프로세스 안에서만 동작하는 것이고
(그러니까 하나의 프로세스안에 여러개의 쓰레드가 있을경우 그 쓰레드들간의 상호배제를 구현하는것이지요)
뮤텍스는 문자열로 뮤텍스의 이름을 지정해 주면 뮤텍스의 이름으로 프로세스간에도 상호배제를 사용할 수 있습니다..
윈도우즈의 뮤텍스와 관련된 함수는 다음과 것들이 있습니다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName);
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseMutex(HANDLE hMutex);
CreateMutex()함수는 뮤텍스를 생성합니다.
이미 생성된 이름을 갖는 뮤텍스의 핸들을 얻기 위해서는 OpenMutex()를 사용합니다.
뮤텍스의 사용이 끝나서 해당 뮤텍스를 놓아 줄때는 ReleaseMutex()함수를 사용합니다.
위에 원형은 밝히지 않았지만 생성한 뮤텍스를 파괴시킬때는 모든 커널객체가 그렇듯 CloseHandle()함수를 사용합니다.
참고로 신호상태인 뮤택스를 얻기 위해서는 위에서 설명한 대기함수를 사용해야 한다는 것은 짐작하셨겠죠?
이때 CreateMutex()는 이미 다른 프로세스에서 사용하고자 하는 뮤텍스를 생성하였다면 CreateMutex()함수를 호출하지 않고 OpenMutex()를 사용해도 되지만 CreateMutex()함수를 다시 한번 더 호출해도 상관이 없습니다.
CreateMutex()함수의 아큐먼트는 다음과 같습니다.
- lpMutexAttributes : 뮤텍스의 보안 속성을 지정하는 것으로서 주로 상속관계를 지정하기 위해 사용됩니다. 일반적으로는 NULL을 입력합니다.
- bInitialOwner : 뮤텍스를 생성하면서 사용권한을 갖을 것인지를 결정합니다. 즉, TRUE를 입력하면 생성하자 마자 뮤텍스의 사용권한(비신호상태의 뮤텍스 생성)을 갖습니다. FALSE를 입력할 경우 뮤텍스만 생성하기만(신호상태의 뮤텍스 생성) 하고 실제 사용권한을 얻을때는 대기함수를 사용해야 합니다.
- lpName : 뮤텍스에 이름을 문자열로 지어 줍니다. 이름이 지정된 뮤텍스를 만듦으로서 이 이름을 아는 다른 프로세스와의 동기화를 할 수 있습니다. 하나의 프로세스에서만 동기화 방식으로 뮤텍스를 사용한다면 NULL을 입력하여 이름을 지어 주지 않을 수도 있습니다. 참고 이름을 지어준 뮤텍스를 명명된 뮤텍스(named mutex)라고 합니다.
CreateMutex()함수는 생성한 뮤텍스의 핸들을 리턴합니다. 만약 생성하려는 뮤텍스의 이름으로 이미 뮤텍스가 생성되어 있는 경우는 해당 뮤텍스 핸들을 리턴하고 GetLastError()로는 ERROR_ALREADY_EXISTS값을 얻을 수 있습니다. 만약 생성되어 있는 뮤텍스에 접근할 수 있는 권한이 없는 경우(ERROR_ACCESS_DENIED) NULL을 리턴합니다. 구체적인 원인을 알기 위해서는 GetLastError()함수를 호출하여 알 수 있습니다. 만약 EROR_ACCESS_DENIED일 경우는 OpenMutex()함수를 사용하여야 합니다.
OpenMutex()함수의 아큐먼트는 다음과 같습니다.
- dwDesiredAccess : 접근속성
- bInheritHandle : 상속옵션
- lpName : 지정된 뮤텍스의 이름
OpenMutex()함수는 이름이 지정된 뮤텍스의 핸들의 핸들을 리턴합니다. 만약 지정된 이름의 뮤텍스가 없는 경우는 NULL을 리턴하고 GetLastEror()함수는 ERROR_FILE_NOT_FOUND값을 리턴합니다.
일반적으로 OpenMutex()함수는 잘 사용하지 않습니다. 왜냐하면 CreateMutex()함수로 새로운 뮤텍스를 생성할 수도 이미 생성된 뮤텍스의 핸들을 얻을 수도 있기 때문이죠.
위에서 이야기 한대로 뮤텍스에 보안 속성을 설정한 경우 OpenMutex()를 사용하기도 합니다.
ReleaseMutex()함수는 잡았던 뮤텍스(비신호상태의 뮤텍스)를 다시 놓아주는(신호상태로 만들어 주는) 함수입니다.
아큐먼트는 hMutex에 뮤텍스 핸들을 넣어 줍니다. 놓아주기에 성공하면 TRUE를 실패하면 FALSE를 리턴합니다.
뮤텍스 설명에 관한 출처...
http://blog.naver.com/herryjoa?Redirect=Log&logNo=110028833001
-----------------------------------------------------------------------------------------------------------------------
2 크리티컬섹션 (Critical section) #
유저모드 동기화 객체
커널모드 객체가 아니기 때문에 가볍고 같은 프로세스내에 스레드 동기화에 사용할 수 있다.
EnterCriticalSection을 호출하면 객체는 비신호 상태가 되고,
LeaveCriticalSection을 호출하면 신호상태로 바뀌어서 다른 스레드들이 접근가능하다.
커널모드 동기화 객체
커널모드라서 크리티컬 섹션보다는 느리지만 프로세스를 넘어서 모든 스레드에 사용 될 수 있는 동기화 객체이다.
뮤텍스를 신호상태로 생성한 후 스레드에서 Wait 함수를 호출하면 뮤텍스는 비신호 상태가 되어서 다른 스레드에서는 접근하지 못한다.
ReleaseMutex를 호출하면 뮤텍스는 신호상태가 되어 다른 스레드들이 접근가능하다.
4 세마포어 (Semaphore) #
커널모드 동기화 객체
뮤텍스와 비슷하지만 접근할 수 있는 스레드 갯수를 정할 수 있다.
세마포어를 생성할 때 3개의 스레드들이 접근가능하도록 지정하면 내부카운트값은 3이다.
객체 내부적으로 카운트를 관리하여 세마포어 객체를 Wait하는 스레드가 있으면 카운트가 하나씩 감소한다. 그래서 내부카운트가 0이되면 비신호상태로 바뀐다. [[br] 세마포어를 사용하고 있는 스레드들중 ReleaseSemaphore 하면 세마포어 내부카운트는 다시 1 증가하여 신호상태로 바뀌어서 다른 스레드들이 사용가능하게 된다.
※ 세마포어 생성 시 접근 가능한 스레드를 0으로 설정해서 WaitforSingleObject와 같은 효과를 내어서 사용하기도 하죠.
출처: http://pastime0.tistory.com/entry/CreateMutex [놀러와]
=======================
=======================
=======================
STL은 멀티 쓰레드에 안전하지 않습니다.
STL은 여러 쓰레드가 하나의 컨테이너(vector, list, map, string..)을 동시에 사용시 무결성이 깨질수 있는 약점을 가지고 있습니다. 따라서, 사용자는 멀테쓰레드를 사용시에는 컨테이너에 락처리를 손수 해줘야 합니다. 일반적으로 멀티쓰레드에 대한 락처리는 뮤텍스(Mutex), 세마포어(Semaphore), 크리티컬섹션(Critical Secion)을 사용할수 있는데, 여기서는 뮤텍스를 사용해서, 락 처리가 된 클래스를 만들어서 사용해 보겠습니다.
다음 클래스는 앞서 언급한 멀티쓰레드에서 동기화 처리를 위한 간단한 예제입니다.
방식은 생성자에서 락객체(m_hMutex)를 생성하고, lock(), unlock() 함수를 호출하여 락처리를 수행하며, 소멸자에서 생성된 락객체를 삭제합니다.
// Lock.h
#ifndef LOCK_HEADER
#define LOCK_HEADER
class Lock
{
public:
//생성자에서 lock을 생성
Lock() : m_hMutex(NULL)
{
m_hMutex = ::CreateMutex( NULL, false,NULL);
}
//소멸자에서 lock을 소멸
virtual ~Lock()
{
::CloseHandle( m_hMutex );
}
bool lock () //락처리
{
if ( m_hMutex == NULL )
return false;
WaitForSingleObject( m_hMutex, INFINITE );
return true;
}
void unlock () //락해제
{
ReleaseMutex(m_hMutex);
}
private:
HANDLE m_hMutex; //lock, unlock에 사용될 객체
};
#endif
이번에는 실제 STL의 컨테이너에 위의 클래스(Lock)를 적용하여 동기화를 구현해 보겠습니다.
// LockVector.h
#ifndef LOCK_VECTOR
#define LOCK_VECTOR
#include "Lock.h"
#include <vector>
using namespace std;
template <class T>
class LockVector : vector<T>, Lock
{
public:
LockVector () : vector<T>(), Lock()
{
}
virtual~ LockVector ()
{
}
void push_back(const T& obj)
{
if (!lock())
return;
vector<T>::push_back (obj);
unlock();
}
};
#endif
소스에서 보는 것처럼, Lock 클래스와 STL 컨테이너를 다중 상속 받습니다. 이제, 삽입과 삭제에 관련된 메소드는 오버라이딩 해서, 멀티쓰레드 상에서도 안전하게 동작하도록 락처리를 적용 합니다. 여기서는 push_back 함수 하나만 오버라이딩 했지만, 컨테이너내 항목을 삭제하나거 추가하는 함수는 필요시 락처리가 되도록 오버라이딩해서 사용해야 합니다.
다음은 간단한 사용예제 입니다.
// 간단한 LockVector 사용예
#include <iostream>
#include "LockVector.h"
#include <iostream>
int main()
{
LockVector<int> Lv;
Lv.push_back(1);
cout << Lv.at(0) << endl;
return 1;
}
출처: http://bestend.co.kr/209 [Jun`s Story]
=======================
=======================
=======================
기타링크:
- https://www.slideshare.net/zzapuno/kgc2013-3
=======================
=======================
=======================
'프로그래밍 관련 > 언어들의 코딩들 C++ JAVA C# 등..' 카테고리의 다른 글
간단한 자바 소켓 프로그래밍 관련 (0) | 2020.09.13 |
---|---|
java Thread 관련 사용 방법 쓰기 정지 방법 관련. (0) | 2020.09.13 |
C/C++ 스마트 포인터 관련 (0) | 2020.09.13 |
c 그리고 자바 연동관련 dll 연동 관련 (0) | 2020.09.13 |
델파이 그리고C 혼합 dll 관련 이미 만들어진 델파이 DLL과 C++Builder와 Delphi 유닛을 혼합해서 쓸때 방법(보강) (0) | 2020.09.13 |