타이머 기반 동기화 -------------------------------------------------------------------------------------------

커널 오브젝트의 signaled 상태는 매우 중요한 개념이다. 왜냐하면 어떠한 커널 오브젝트도 특정상황이 도래하면 signaled 상태가 되기 때문이다.

타이머 기반 동기화란 그 점을 이용하여 타이머 오브젝트라는 것을 생성하고 그 오브젝트가 일정시간이 지나면 하나의 자동으로 signaled상태가 되는 것을 의미한다.


타이머 기반 동기화의 종류 -------------------------------------------------------------------------------------

타이머 기반동기화는 2가지가 존재한다.

1. 수동 리셋 타이머 : 알람이라고 생각하면 된다. 시간을 맞춰놓으면 그 시간에 특정 동작을 실행하는 타이머 오브젝트를 의미한다.

2. 주기적 타이머 : 수동리셋 타이머에 주기적인 특성이 추가된 오브젝트 특정 시간에 맞춰놓으면 그 시간에 특정 동작이 실행되고 그 이후로 또 특정 시간마다 다시 특정 동작을 실행하는 오브젝트를 의미한다.


타이머 오브젝트 생성함수 --------------------------------------------------------------------------------------

HANDLE CreateWaitableTimerf (

LPSECURITY_ATTRIBUTES lpTimrAttributes, -> 보안속성 지정 핸들을 자식 프로세스에 상속할경우는 그에 맞는 값 아닌 경우는 NULL

BOOL bMaunalReset, -> 타이머 오브젝트를 수동리셋모드로 생성할 것인지, 자동리셋모드로 생성할 것인지 결정하는 함수

LPCTSTR lpTimerName -> 타이머 오브젝트에 이름을 붙여줄 경우 사용하는 전달인지 NULL시 이름없는 오브젝트를 생성한다.

);


타이머 오브젝트 시간설정 함수 ---------------------------------------------------------------------------------

BOOL SetWaitableTimer (

HANDLE hTimer, 

const LARGE_INTEGER* pDueTime, 

LONG lPeriod, 

PTIMERAPCROUTINE pfnCompletionRoutine, 

LPVOID lpArgToCompletionRoutine,

BOOL fResume 

);


HANDLE hTimer, -> 알람을 설정할 타이머 오브젝트의 핸들을 인자로 전달한다. 정해진 시간이 되면 이 핸들의 커널 오브젝트는 signaled상태가 된다.

const LARGE_INTEGER* pDueTime, -> 알람 시간을 지정한다. +라면 절대시간을 의미하고 -값이 전달되면 상대시간을 의미하게 된다. 

ex1) "A시 B분에 알람을 울려주세요" +로 시간설정

ex2) "지금으로부터 A초 후에 알람을 울려주세요" - 값을 넣어주면 10억부늬 1초 단위로 시간을 설정한다.

LONG lPeriod, 타이머가 주기적으로 알람을 울리게 할때 전달하는 인자 주기가 되는 시간을 1/1000초 단위로 전달하면 된다. 0을 전달할 경우 주기적을 알람을 사용하지 않는다는 의미이다.

PTIMERAPCROUTINE pfnCompletionRoutine, 

LPVOID lpArgToCompletionRoutine,

-> 루틴 타이머를 설정하는 함수 추후 설명

BOOL fResume -> 컴퓨터 전원 관리와 관련 있는 매개변수이다. 기본적으로 FALSE 전달을 원칙으로 한다.


타이머 정지 함수 ---------------------------------------------------------------------------------------

BOOL CancelWaitableTimer (

HANDLE hTimer

);

hTimer, 알람을 해제할 타이머 오브젝트의 핸들을 전달한다. 전달된 핸들의 타이머 오브젝트는 알람이 해제된다. Inactive 상태가 된다.

Posted by JJOREG

이벤트 PULS 뮤텍스? -----------------------------------------------------------------------------------------

동기화 방법이 여러가지 있는 관계로 당연히 혼용하여 쓸수 있게 된다.

이벤트 오브젝트를 통하면 작업과정은 동일하게 할수 있어도 소비자가 2명이라면 그 2명의 소비자가 동시에 가게에 들어오는 경우가 생길 수가 있다. 그런 과정 자체가 혹은 소비자가 3명일 수도 혹은 4명일수도 있는 것이다.

즉 그 2명의 소비자의 동기화를 위해서 뮤텍스를 사용하는 방식을 보여준다.


unsingned int WINAPI OutPutTAhreadFunction1(LPVOID lpParam) -> 첫번째 쓰레드 함수

unsingned int WINAPI OutPutTAhreadFunction2(LPVOID lpParam) -> 두번째 쓰레드 함수


unsingned int WINAPI OutPutTAhreadFunction1(LPVOID lpParam)

{

WaitForSingleObject(hEvent, INFINITE); -> 메인쓰레드에서 작업하는 내용을 작업해주고 이벤트 동기화를 먼저 해준다음

WaitForSingleObject(hMutex, INFINITE); -> 뮤텍스를 통해서 작업결과물에 2개의 쓰레드가 동시에 접근하는 것을 동기화 시킨다.

코드 내용 전역변수 사용

ReleaseMutex(hMutex)

}

unsingned int WINAPI OutPutTAhreadFunction2(LPVOID lpParam)

{

WaitForSingleObject(hEvent, INFINITE); -> 메인쓰레드에서 작업하는 내용을 작업해주고 이벤트 동기화를 먼저 해준다음

WaitForSingleObject(hMutex, INFINITE); -> 뮤텍스를 통해서 작업결과물에 2개의 쓰레드가 동시에 접근하는 것을 동기화 시킨다.

코드 내용 

ReleaseMutex(hMutex)

}

Posted by JJOREG

실행 순서의 동기화 -----------------------------------------------------------------------------------------


이전까지 하나의 공유객체에 대해서 접근하는 방법에 대해서 알아보았다.
다음은 쓰레드의 실행순서를 동기화하는 차례이다.
쓰레드의 연산이나 메모리에 접근하는 실행순서를 동기화 한다.

A, B, C쓰레드가 있다고 할때 A의 연산결과를 B가 받아서 연산하고 B의 연산결과를 C가 받아서 연산해야 한다면 A B C는 순차적으로 실행되어야 한다. 이러한 상황을 위해서 실행 순서의 동기화가 필요하다. 순차적으로 실행되지 않는다면 분명 원하지 않는 값이 나오거나 이상한 값이 나올것이 뻔하다.


이벤트 기반의 동기화 -----------------------------------------------------------------------------------------

이벤트 기반의 동기화 모델에 있어서 프로그램의 추상화 개념과 비슷하게 비교해 본다면. A와 B쓰레드가 존재하고 A 쓰레드는 생산자, B 쓰레드는 소비자라는 개념을 든다.

"A생산자가 특정 물건을 만들기 전까지는 B소비자는 물건을 소비할 수 없다."

다른 비유를 들어보다면 이런 느낌을 가질 수 있습니다.

"A생산자가 가게문을 열기전까지 B소비자는 가게안에 들어와 물건을 구매할 수 없다."

커널기반이므로 당연히 윈도우에서 이에대한 지원 함수가 존재합니다.

-> 사용 방식

HANDLE CreateEvent(

  LPSECURITY_ATTRIVUTES lpEventAttributes, 

  BOOL bManualReset,

  BOOL bInitialState,

  LPCTSTR lpName

);

  LPSECURITY_ATTRIVUTES lpEventAttributes, -> 보안속성 지정

  BOOL bManualReset,-> 가장 중요한 전달인자 수동리셋모드로 이벤트 오브젝트를 생성하느냐, 자동 리셋 모드로 이벤트 오브젝트를 생성하느냐를 결정짓는다. TRUE로하면 수동리셋모드 FALSE가 전달될 경우 자동리셋 모드로 이벤트의 커널 오브젝트가 생성된다.

  BOOL bInitialState -> 이벤트 오브젝트의 초기상태를 설정한다. TRUE가 전달될 경우 Signaled 상태의 이벤트 오브젝트가 생성되고, FALSE가 전달될 경우 Non-Signaled 상태의 이벤트가 생성된다.

  lpName 이벤트 오브젝트에 이름을 줄 경우에 사용하는 전달인자이다. NULL을 전달하면 이름 없는 이벤트 오브젝트가 생성된다.


-> 종료 방식

  이벤트 오브젝트를 소멸시킬 때에는 다른 커널 오브젝트와 마찬가지로 CloseHandle 함수를 사용하면 된다.


 여기서 잠깐! WaitForSingleObject -----------------------------------------------------------------------------

 커널 오브젝트를 읽을때 그냥 넘어갔던 함수가 있었는데 여기서 다시 설명해야할 듯 하다. WaitForSingleObject함수가 존재하는데 이 함수는 핸들을 인자로 적용해서 커널 오브젝트의 상태를 확인하는데 사용되는 함수이다. 

 DWORD WaitForSingleObject {

HANDLE hHandle,         -> 커널 오브젝트의 핸들

DWORD dwMilliseconds -> 커널오브젝트가 종료될때까지 기다려주는 값 밀리세컨트단위를 사용한다.

}

이 함수가 반환하는 값은 다음과 같다.

 Priority 

 meaning 

 WAIT_OBJECT_0

 커널 오브젝트가 Signaled 상태가 되면 반환하는 값

 WAIT_TIMEOUT

 커널 오브젝트가 Signaled 상태가 되지 않고, dwMilliseconds 인자를 통해서 설정된 시간이 다 된 경우에 반환하는 값

 WAIT_ABANDONED

 소유 관계와 관련하여 함수가 정상적이지 못한 오류 발생에 의해서 반환되는 값


이벤트 기반의 동기화의 개념 -----------------------------------------------------------------------------------

  앞선 설명으로 커널 오브젝트는 이벤트 커널 오브젝트는 다른 쓰레드나 프로세스의 커널 오브젝트와 달리 자동으로 Signaled상태가 되지 않는다. 순서로 설명하자면.

 1. 이벤트 오브젝트 핸들을 전역으로 선언

 2. 메인 쓰레드가 이벤트 오브젝트를 생성한다.

 3. 쓰레드 A를 생성한다.

 4. 쓰레드 A에서 hEvent WaitForSingleObject를 호출한다. 이 의미는 이벤트가 signaled가 되기를 기다리는 것을 의미한다.

 5. 메인쓰레드에서는 쓰레드A의 핸들로 WaitForSingleObject를 호출한다.

 이 경우 4번의 과정에서 WaitForSingleObject의 호출로 signaled상태가 된 이벤트 오브젝트는 어떠한 상태가 될 것인가?

 signaled인가? Non-signaled인가?  

 정답은 BOOL bManualReset인자가 TRUE(수동 리셋 모드)라면  signaled.

 bManualReset인자가 FALSE(자동 리셋 모드)라면  Non-signaled이다.


정리하자면.

1. 이벤트 커널 오브젝트는 프로그래머의 요청에 의해서 Signaled 상태가 된다.

2. Non-Signaled 상태의 이벤트 오브젝트 때문에 WaitForSingleObject 함수 호출이 블로킹 되었다면, Signaled 상태가 되는 순간 블로킹된 함수를 빠져 나오게 된다. 그리고 이때 자동 리셋 모드 이벤트 오브젝트라면 Non-Signaled 상태로의 변경은 자동으로 이루어 진다.

3. 이중 수동 리셋모드는 둘 이상의 쓰레드를 동시에 깨워서 실행해야 할 때 아주 좋은 도구가 될 수 있다.

Posted by JJOREG

뮤텍스 기반의 동기화 -----------------------------------------------------------------------------------------

크리티컬섹션 기반의 동기화방식과 비슷하다.

위와 같은 함수를 사용하며 핸들을 반환하는 것을 보면 알겠지만 핸들을 반환하며 초기화 함수가 필요없다.


앞서 커널오브젝트의 상태에 대해서 설명한 적이 있는데. 뮤택스 또한 커널오브젝트를 생성한다. 뮤텍스의 커널 오브젝트는 누군가가 열쇠를 취득했을때 Non-Signaled상태가 되고 열쇠가 반환되면 Signaled상태가 가 된다. 그리고 이러한 상태를 이용하여 동기화가 가능하다.

즉 커널오브젝트에서 설명했던 WaitForSingleObject함수를 임계영역 진입을 위한 뮤텍스 획득의 용도로 사용이 가능하게 된다.

뮤텍스도 커널오브젝트이며 핸들이 존재하므로 해제가 필요하며 이는 윈도우에서 함수로 존재한다.

BOOL ReleaseMutex ( HANDLE hMutex ) 함수를 통하여 해제가 가능하며 뮤텍스는 다시 Signaled 상태가 된다.

위의 설명의 이해를 돕기 위해서 아래와 같이 코드를 예제로 들어본다.

HANDLE hMutex; -> 이 핸들을 이요하여 뮤텍스를 생성한다고 본다.

LONG g_count = 0;

void IncreaseCount()

{

// EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

WaitForSingleObject (hMutex, INFINITE);

g_count++;

// LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

ReleaseMutex (hMutex) ;

}

식으로 이용이 가능하다

WaitForSingleObject함수는 위와 같은 방식 말고도 다양한 방식으로 이용이 가능한데 아래와 같은 함수를 만들어 호출하는 방식으로도 이용이 가능하다.

DWORD AcquireMutex( HANDLE hMutex )

{

return WaitForSingleObject (hMutex, INFINITE);

}

간혹 함수를 한번 호출한다는 이유로 이런 코드를 부정적으로 바라보는 경우가 있는데, 컴파일러의 최적화로 인해서 함수의 호출 문장을 함수 의 몸체로 대치시켜 버리는 경우가 생긴다. 즉 코드를 통해서 의미를 전달할 뿐이지 실제 작동은 그냥 함수를 적용시킬 때랑 동일하게 적용 된다.


세마포어 기반의 동기화 ---------------------------------------------------------------------------------------

"세마포어 중에서 단순화된 세마포어(바이너리 세마포어를 의미한다.)를 가르켜 뮤텍스라고 한다"

뮤텍스는 세마포어의 일종이라고 말할 수가 있다.

하지만 뮤텍스와 세마포어는 차이가 있는데. 그 차이는 카운트(Count) 기능이라고 할 수 있다. 세마포어는 카운트 기능이 존재하지만 뮤텍스에는 존재하지 않는다.

카운트 기능이란 임계영역에 접근 가능한 쓰레드의 개수를 조절하는 기능을 의미한다.

HANDLE CreateSemaphore ( 

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

LONG lInitialCount,

LONG lMaximumCount,

LPCTSTR lpName

);

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

-> 보안속성을 지정하기 위한 매개변수

LONG lInitialCount,

-> 세마포에서 가장 중요한 전달인자이다. 세마포어는 값을 지니는데 이 값을 기준으로 접근 가능한 쓰레드의 개수를 설정한다.

LONG lMaximumCount,

세마포어가 지닐수 있는 값의 최대 크기를 지정한다. 이 값이 1일 경우에는 뮤텍스와 동일한 기능을 하는 바이너리 세마포어가 구성된다. 기본적으로 lInitialCount인자 값보다 큰 값을 지녀야 한다.

LPCTSTR lpName

세마포어에 이름을 붙이기 위해서 사용한다. 커널 오브젝트가 이름을 지닐 때 어떠한 용도로 사용되는지 잠시 후에 소개한다.


이렇게 할당된 세마포어는 당연히 커널 오브젝트를 가지게 되며, 해제를 위해서는 다음의 함수를 사용해야 한다.

BOOL ReleaseSemaphore (

HANDLE hSemaphore,

LONG lReleaseCount,

LPLONG lpPreviousCount

);

HANDLE hSemaphore,

-> 반환하고자 하는 세마포어의 핸들을 인자로 전달한다.

LONG lReleaseCount,

-> 반환한다는 것은 lInitialCount 카운트의 증가를 의미한다. 이 전달인자를 통해서 증가시킬 값의 크기를 결정할수 있다. 만약 lMaximumCount를 넘겨서 증가시킨다면 카운트는 변경되지 않고 FALSE만 반환한다.

LPLONG lpPreviousCount 변경되기 전 세마포어 카운트값을 저장할 변수를 저장한다. 필요 없다면 NULL을 전달한다.


실제 세마포어의 이용


HANDLE hSemaphore; -> 이 핸들을 이요하여 세마포어를 생성한다고 본다.

LONG g_count = 0;

void IncreaseCount()

{

WaitForSingleObject (hSemaphore, INFINITE);

g_count++;

ReleaseMutex (hSemaphore, 1, NULL) ;

}


WaitForSingleObject 의 최대값? -------------------------------------------------------------------------------

WaitForSingleObject 함수는 최대 64개의 커널 오브젝트를 관찰할 수 있다.  이는 이 함수에 설정되어있는 매크로 값인 MAXIMUM_WAIT_OBJECT 값이 64로 제한되어 있기 때문이다.


이름 있는 뮤텍스기반의 프로세스 동기화 -----------------------------------------------------------------------

앞서 설명하기에 커널오브젝트는 운영체제에 종속적으로 표현되었다. 즉 뮤텍스란 프로세스에 속한 녀석이 아니기 때문에 뮤텍스에 이름을 붙이고 프로세스들이 이 이름을 알고만 있다면 모든 프로세스들의 쓰레드가 이 뮤텍스를 이용할 수가 있을 것이다.

이런 점에서 이름있는 뮤텍스와 없는 뮤텍스의 차이점이 발생한다. 이름있는 뮤텍스를 정리한다면.

서로다른 프로세스에 있는 두개의 쓰레드를 동기화가 가능하다.


하지만 위와 같이 전혀 관련이 없는 각 프로세서끼리는 핸들등의 상속또한 불가능하기 때문에 한쪽에서 생성된 뮤텍스에 접근이 불가능 하다. 이런 점을 위해서 뮤텍스에 이름을 붙이게 되는 것이다. 앞서 설명했던 이름 업는 뮤텍스와 같이 일단 CreateMutex 함수를 사용해야 한다.

CreateMutex (

LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner,

LPCTSTR lpName -> 이부분에 이름을 붙여줌으로 해서 다른 프로세스가 뮤텍스에 접근할 수 있게 된다.

)

뮤텍스에 접근을 하기 위해서는 OpenMutex라는 함수가 필요하다.

HANDLE OpenMutex (

DWORD dwDesiredAccess,

BOOL bInheritHandle,

LPCTSTR lpName -> 다른 프로세스에서 생성한 뮤텍스의 이름을 넣어줌으로서 다른 프로세스에 존재하는 뮤텍스를 오픈 할 수 있다.

);


 여기서 잠깐! WaitForSingleObject -----------------------------------------------------------------------------

 커널 오브젝트를 읽을때 그냥 넘어갔던 함수가 있었는데 여기서 다시 설명해야할 듯 하다. WaitForSingleObject함수가 존재하는데 이 함수는 핸들을 인자로 적용해서 커널 오브젝트의 상태를 확인하는데 사용되는 함수이다. 

 DWORD WaitForSingleObject {

HANDLE hHandle,         -> 커널 오브젝트의 핸들

DWORD dwMilliseconds -> 커널오브젝트가 종료될때까지 기다려주는 값 밀리세컨트단위를 사용한다.

}

이 함수가 반환하는 값은 다음과 같다.

 Priority 

 meaning 

 WAIT_OBJECT_0

 커널 오브젝트가 Signaled 상태가 되면 반환하는 값

 WAIT_TIMEOUT

 커널 오브젝트가 Signaled 상태가 되지 않고, dwMilliseconds 인자를 통해서 설정된 시간이 다 된 경우에 반환하는 값

 WAIT_ABANDONED

 소유 관계와 관련하여 함수가 정상적이지 못한 오류 발생에 의해서 반환되는 값

하나의 함수가 더있는데 이녀석은 상태를 확인하려고 하는 커널오브젝트가 둘 이상이고 이들의 핸들이 배열로 묶여있다면 활용하기 편리하다.

 DWORD WaitForMultipleObject {

DWORD nCount,         -> 배열에 저장되어있는핸들의 개수를 전달한다.

const HANDLE* lpHandles, -> 핸들을 저장하고 있는 배열의 주소 정보를 전달한다. 이 주소값을 시작으로 총 nCount개의 핸들을 관찰 대상으로 둔다.

BOOL bWaitAll            -> 관찰 대상이 모두 Signaled 상태가 되기를 기다리고자 하는지 TRUE전달시, 아니면 하나라도 Signaled상태가 되면 반환할 것인지(FALSE, 전달시)를 결정 짓는다.

DWORD dwMilliseconds -> 커널오브젝트가 종료될때까지 기다려주는 값 밀리세컨트단위를 사용한다.

}


뮤텍스의 소유와 WAIT_ABANDONED -----------------------------------------------------------------------

WaitForSingleObject 함수의 반환값을 보면 WAIT_ABANDONED라는 값이 있다. 

A와 B 쓰레드가 있고 세마포어 오브젝트 C가 있다고 가정해 보면 A 쓰레드가 세마포어 C의 카운트를 하나 감소시켰다. 

그렇다면 다시 카운트를 증가시키는 쓰레드는 A가 된다. 그러나 이러한 제약사항은 뮤텍스 하나만이 지켜주어야 한다.

즉 뮤텍스는 획득한 쓰레드가 직접 반환하는 것이 원칙이다. 현재 뮤텍스를 소유한 본인만이 반환이 가능하다. 그러나 세마포어나 다른 동기화 오브젝트는 현재 커널오브젝트를 소유한 다른 쓰레드가 반환해도 문제가 되지 않는다.

위에 설명한 상황을 좀더 자세히 설명하자면, A쓰레드 B쓰레드 C뮤텍스가 있다고 했을때 쓰레드 A가 뮤텍스 C를 회득했했다면 쓰레드 B는     쓰레드 A가 뮤텍스C를 반환하기를 기다릴 것이다. 그런데 갑자기 예상못한 일이 생겨서 쓰레드 A가 종료되어 버렸다. 즉 뮤텍스를 소유하고 있는 쓰레드가 뮤텍스를 반환하지도 않고 사라진 것이다. 

  이런 상황이 오면 윈도우운영체제는 더이상 정상적인 방법으로 반환이 불가능한 뮤텍스를 대신 반환해주고 다음대기자인 쓰레드 B가 뮤텍스를 소유할 수 있도록 도와준다. 이때 쓰레드 B는 WAIT_ABANDONED값을 반환받게 된다.

 WaitForSingleObject 함수의 반환 값 WAIT_ABANDONED

Posted by JJOREG

쓰레드 동기화란? ---------------------------------------------------------------------------------------------

앞서 설명되었던 쓰레드간의 실행순서의 문제를 해결하기 위한 방법을 의미한다.

쓰레드 동기화는 2가지 관점에서 볼수 있다.

1. 실행순서의 동기화

A쓰레드가 계산한 결과를 B쓰레드가 받아서 처리해야하는 구조라면 반드시 A쓰레드가 실행된후 B쓰레드를 실행시키는 방식을 의미.

A -> B -> C -> D 순서로 쓰레드를 실행하고 그것이 반드시 지켜진다면 순서의 동기화 이다.

2. 메모리 접근에 대한 동기화.

하나의 메모리 접근에 있어서 2개의 쓰레드가 동시에 접근하는 것을 막는 것 또한 쓰레드의 동기화에 속한다.

좀더 직관적으로 설명한다면 한순간에 하나의 쓰레드만 메모리에 접근이 가능하게 하면 된다.

이 두가지는 경우는 보통 동시에 이루어진다. 쓰레드가 메모리에 접근하는 실행순서를 동기화 한다는 말로 표현이 가능하겠다.


동기화의 2가지 방법 -----------------------------------------------------------------------------------------

유저모드 동기화와 커널모드 동기화의 2가지 방법이 존재한다.

유저모드 동기화 -> 커널의 힘을 빌리지 않고(커널모드가 실행되지 않은 상태로 동기화 한다.) 일반적으로 커널모드의 실행은 그 자체로 연산이 필요하기 때문에 유저모드는 커널모드보다 성능상의 이점이 있을 수 있으나, 커널모드의 기능을 사용하지 못하므로 기능상의 제한도 수반한다.

커널모드 동기화 -> 유저모드 동기화와는 정반대이다. 성능은 저하될 수 있으나 운영체제에서 지원하는 함수나 기능을 이용할수 있다.

두가지 동기화 모드에는 당연히 일장일단이 있다고 본다.


임계 영역 접근 동기화 ----------------------------------------------------------------------------------------

임계영역이란?

배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 모든 쓰레드가 공유해야하는 리소스(전역변수 같은)에 접근하는 코드 블록을 의미한다.


동기화의 종류와 분류 -----------------------------------------------------------------------------------------

1. 크리티컬 섹션 기반 동기화 (메모리 접근 동기화에 사용하는 예를 책에서 보임)

2. 인터락 함수 기반의 동기화 (메모리 접근 동기화에 사용하는 예를 책에서 보임)

-------------------------- (여기까지가 유저모드에서의 동기화)

3. 뮤텍스 기반의 동기화. (메모리 접근 동기화에 사용하는 예를 책에서 보임)

4. 세마포어 기반의 동기화. (메모리 접근 동기화에 사용하는 예를 책에서 보임)

5. 이름있는 뮤텍스 기반의 프로세스 동기화. (프로세스간 동기화에 사용한다.)(정확히 이야기 한다면 쓰레드 기반이다.)

6. 이벤트 기반의 동기화. (실행순서 동기화에 사용할 예정)


크리티컬 섹션(Critical Section) 방식의 동기화 -------------------------------------------------------------------

각 쓰레드에 하나의 열쇠를 주고 각 쓰레드가 열쇠를 돌려쓰면서 열쇠를 가지지 않았다면 메모리에 접근하지 못하게 하는 방식.

CRITICAL_SECTION 구조체의 변수를 하나 선언한다.

CRITICAL_SECTION _tCs 선언했다고 하면,

이 변수를 초기화하기 위해서는 반드시 다음의 함수를 통해서 초기화 해야 한다.

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)   라는 함수를 통해서 초기화 해야한다.

InitializeCriticalSection(&_tCs)식으로 초기화 하면 된다.

A B C의 쓰레드가 있다고 치고 이 쓰레드들은 이제 저 열쇠를 통해서 임계영역에 접근해야 하고, 접근 후에는 저 열쇠를 반환해야 한다. 열쇠를 획득하는 함수와 반환하는 함수는 다음과 같다.

획득함수 -> void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)

반환함수 -> void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)

이렇게 초기화 시킨 _tCs구조체는 최종적으로 삭제를 해줘야하며 함수는 아래와 같다.

DeleteCriticalSection(_tCs);


이 함수를 통해 하나의 메모리에 접근하려는 쓰레드를 막는 것은 다음과 같이 할수 있다.

CRITICAL_SECTION _tCs 선언;

LONG g_count = 0;

void IncreaseCount()

{

EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

g_count++;

LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

}

식으로 이용이 가능하다.


크리티컬 섹션(Critical Section) 방식의 동기화 -------------------------------------------------------------------

 앞의 예제와 같인 전역변수 하나에 대한 접근을 동기화 하는 것이 목적일때 특화된 함수가 인터락함수이다.

LONG InterlockedIncrement ( LONG volatile* Addend );

LONG InterlockedDecrement( LONG volatile* Addend );

CRITICAL_SECTION _tCs 선언;

LONG g_count = 0;

void IncreaseCount()

{

InterlockedIncrement ( &g_count );

}

이런 방식으로 사용이 가능하다. 하지만 이함수는 값을 하나씩만 증가시키는 역할을 하는데. MSDN을 보면 원하는 만큼 증가시키거나 64비트 변수를 대상으로 연산하는 함수도 있다.

이러한 유저모드에 대해서 의문을 가질만한 사항은 위의 함수들은 어째서 쓰레드간 문제를 일으키지 않느냐에 대한 궁금증이 있을 것이다.

이는 아래의 인터럽트 Enable/Disable과 관련이 있다 인터럽트란 아래와 같이 설명된다.

  실행 중인 프로그램을 일시 중단하고 다른 프로그램을 끼워 넣어 실행시키는 것. 인터럽트 요인이 되는 조건이 생겼을 때 실행 중인 프로그램(A)을 중단하여 강제적으로 특정한 주소로 제어를 옮기고, 준비되어 있는 인터럽트 처리 프로그램(B)을 실행시키며, 그 처리가 끝나면 원래의 프로그램으로 되돌아가서 계속 실행시킨다. 프로그램 처리의 효율화, 입출력 장치의 동시 동작 온라인 처리의 효율화를 기할 수 있다. 인터럽트 요인의 종류로는 입출력 종료 인터럽트, 프로그램 인터럽트, 감시 프로그램 호출, 장해 인터럽트 등이 있다

부가설명 volatile* 키워드--------------------------------------------------------------------------------------

1. 첫번째 의미

최적화를 하지 마라. 라는 의미의 키워드 이다. 컴파일러는 최종결과가 같다면 중간과정의 코드를 무시하는 경우가 있다.

a = 10

a = 20

a = 30

a = 10

최종적으로는 a가 10이므로 가장 아래의 코드만 남겨 놓는 경우가 이와 같다. 하지만 과정 자체가 중요한 경우도 존재할 수가 있다.

2. 두번째 의미

메모리에 직접 연산하라. 

CPU의 레지스터는 성능향상을 위해서 캐쉬메모리에 메모리를 저장하는 경우가 있다. 하지만 어떤 프로그램이 지속적으로 실행되어야 하는 경우 혹은 의미가 없어보이는 코드처럼 보이나 의미가 있는 경우가 존재한다.

좀더 자세한 설명을 위해서 위키백과에 대해서 설명하겠다.


C/C++ 프로그래밍 언어에서 이 키워드는 최적화 등 컴파일러의 재량을 제한하는 역할을 한다. 개발자가 설정한 개념을 구현하기 위해 코딩된 프로그램을 온전히 컴파일되도록 한다. 주로 최적화와 관련하여 volatile가 선언된 변수는 최적화에서 제외된다. OS와 연관되어 장치제어를 위한 주소체계에서 지정한 주소를 직접 액세스하는 방식을 지정할 수도 있다. 리눅스 커널 등의 OS에서 메모리 주소는 MMU와 연관 된 주소체계로 논리주소와 물리주소 간의 변환이 이루어진다. 경우에 따라 이런 변환을 제거하는 역할을 한다. 또한 원거리 메모리 점프 기계어 코드 등의 제한을 푼다.

C언어 MMIO에서 적용

주로 메모리 맵 입출력(MMIO)을 제어할 때, volatile을 선언한 변수를 사용하여 컴파일러의 최적화를 못하게 하는 역할을 한다.

static int foo;
 
void bar(void)
{
    foo = 0;
 
    while (foo != 255);
}

foo의 값의 초기값이 0 이후, while 루프 안에서 foo의 값이 변하지 않기 때문에 while의 조건은 항상 true가 나온다. 따라서 컴파일러는 다음과 같이 최적화한다.

void bar_optimized(void)
{
    foo = 0;
 
    while (true);
}

이렇게 되면 while의 무한 루프에 빠지게 된다. 이런 최적화를 방지하기 위해 다음과 같이 volatile을 사용한다.

static volatile int foo;
 
void bar (void)
{
    foo = 0;
 
    while (foo != 255);
}

이렇게 되면 개발자가 의도한 대로, 그리고 눈에 보이는 대로 기계어 코드가 생성된다. 이 프로그램만으로는 무한루프라고 생각할 수 있지만, 만약 foo가 하드웨어 장치의 레지스터라면 하드웨어에 의해 값이 변할 수 있다. 따라서 하드웨어 값을 폴링(poll)할 때 사용할 수 있다.

즉 컴파일러의 최적화가 항상 옳지는 않고 프로그래머의 의도와 컴파일러의 판단은 같지 않을수 있다는 것을 보여준다.

Posted by JJOREG

동기화 없는 쓰레드의 문제점 -----------------------------------------------------------------------------------

4개의 쓰레드가 각기의 연산을 한다고 했을때. 쓰레드들 사이의 메모리 전환이 단순히 프로그램의 라인라인 별로 이루어지지 않기 때문에 하나의 전역 변수를 가지고 다수의 쓰레드가 연산을 하게 된다면 그 연산과정에 있어서 CPU의 레지스터 값이 변경되는 순서가 프로그래머의 의도와는 다르게 움직인다.(아니 확실히 처음 쓰레드를 사용하는 사람의 생각과는 다르게 진행될 것이다.) 

LOAD & STORE 명령어를 제대로 이해했다면 이 부분이 이해될 것이다.


멀티 쓰레드 환경 ---------------------------------------------------------------------------------------------

멀티쓰레드 환경은 비쥬얼 2008버전에서 다음과 같이 설정이 가능하다.


또한 쓰레드 생성에 있어서 _beginthreadex 함수를 통해서 쓰레드를 생성해야 한다. 이함수와 일반 createthread 함수의 차이점은 한가지이다. _beginthreadex함수는 쓰레드 생성에 앞서서 생성하는 쓰레드를 위해서 독립적인 메모리 블록을 할당한다.

당연히 그 메모리 블록을 해제하기 위해서는 _endthreadex( unsigned retval )함수를 호출해야 한다.


쓰레드의 상태변화 --------------------------------------------------------------------------------------------

앞서 설명했던 그림을 한번더 깨내본다.

이 그림은 프로세스의 상태를 표현하기 위해서 예제가된 그림이지만 그 프로세스의 상태를 변경시키는 주체는 프로세스가 아니고 쓰레드 그 자체이다. 달라질 것은 없다. 프로세스안에 들어있는 쓰레드의 상태를 그대로 위의 그림과 연관시켜 생각해보면 간단하기 때문이다.

쓰레드가 생성되며 Start 상태가 되면 일단 ready상태가 되고 running 상태가 되길 기다린다. 그리고 running 상태로 실행중인 쓰레드에게 할당된 다임 슬라이스가 모두다 되면 다시 전환되고 자신이 cpu를 차지하지 않아도 되는 시점이 되면 blocked상태로 변하여 다른 쓰레드에게 CPU의 실행권한을 넘겨주고 다시 ready상태가 되면 된다.


상태변화 함수 --------------------------------------------------------------------------------------------

DWORD SuspendThread( HANDLE hThread ) 

쓰레드의 커널 오브젝트에는 SuspendThread 함수의 호출 빈도수를 기록하기 위한 서스팬드 카운트라 불리는 맴버가 존재한다.(Suspend Count) 현재 실행중인 쓰레드의 서스펜드 카운트는 0이다. 이 함수를 사용할때마다 Suspend Count를 1씩 증가시킨다고 보면 된다. 

Suspend Count는 당연히 반대로 감소시키는 함수가 존재한다.

DWORD ResumeThread( HANDLE hThread ) 

상태의 변화에 의한 내용은 다음과 같다.


Suspend Count가 의미하는 쓰레드의 상태 -----------------------------------------------------------------------

Suspend Count = 0 : 현재 실행중인 쓰레드(Ready)
Suspend Count > 0 : Block 상태

이 함수들은 변경되기 이전에 Suspend Count 를 리턴한다.

쓰레드의 우선순위 컨트롤 :
프로세스의 우선순위는 기존 우선순위라고 표현한다.
쓰레드는 추가로 상대적 우선순위를 갖는다.

 Priority  의미 
 THREAD_PRIORITY_LOWEST -2
 THREAD_PRIORITY_BELOW_NORMAL -1
 THREAD_PRIORITY_NORMAL 0 (Default)
 THREAD_PRIORITY_ABOVE_NORMAL +1
 THREAD_PRIORITY_HIGHEST

 +2



Posted by JJOREG

쓰레드의 소멸 ------------------------------------------------------------------------------------------------

일단 아래의 그림을 보자.


하나의 프로세스가 생성되면 그 프로세스의 메인쓰레드와 메인쓰레드에서 앞서 설명한 쓰레드생성 함수나 몇가지 방법을 통해서 쓰레드가 생성될 것이다. 메인쓰레드에 쓰레드 A B C는 종속적이라고 할수 있다. 이유는 아래 설명한다.

메인쓰레드의 특징은 메인쓰레드가 return 0 명령으로 종료되면 프로세스 그 자체가 종료된다는 것이다. 다른 쓰레드가 하던일을 끝마치기도 전에 종료가 될수 있다. 프로세스가 종료되면 당연히 쓰레드도 종료된다. 

하지만 다른 A B C의 return 문은 자신의 쓰레드(실행흐름)만 종료시키는 결과를 가진다.


쓰레드의 소멸방법 --------------------------------------------------------------------------------------------

1. 각 쓰레드 함수에서 return 문으로 종료하는 방법 -> (가장 이상적인 방법)

2. ExitThread 함수를 사용하는 방법 -> 이 함수는 현재 실행중인 쓰레드를 종료할때 사용한다.

VOID ExitThread( DWORD dwExitCode ); 가 함수의 원형이다.

이 방법의 장점은 언제 어디서나 자신이 원하는 쓰레드를 종료시킬수 있다는 점이다. 또한 다른 프로그래머가 쓰레드의 구조를 파악할때. 이 함수를 보고 쓰레드의 종료시점을 예측하기 편하다.



하지만 한가지 주의할 사항이 있다. C++(내가 사용하는 프로그래밍 언어) A, B 함수의 스택영역에 객체가 존재한다고 할때 C함수에서 ExitThread 함수가 호출된다면 A, B함수의 스택영역에 존재하는 객체의 소멸자가 호출되지 않아서 메모리릭이 날 수가 있다.

3. CreatdThread 함수를 이용해서 얻게되는 핸들을 통해서 다른 쓰레드를 종료하는 방법

만약 쓰레드의 핸들을 가지고 있다음 다음 함수를 이용해 쓰레드를 종료시킬수 있다.

BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );

첫번째 인자는 당연히 종료시킬 쓰레드의 핸들이며 두번째 인자는 해당 쓰레드의 커널 오브젝트에 등록된다.

이 함수의 문제점은 쓰레드를 강제종료 시킨다는 점이다. 다시 말해서 종료의 대상이 되는 쓰레드는 종료가 되는 시점까지도 자신이 종료된다는 사실을 모르므로 당연히 할당된 메모리가 남아있건 어떤 상황이건 종료가 되어버린다.

책에도 나와있지만 그다지 권장하는 방법은 아니다.

Posted by JJOREG

쓰레드 생성 함수 ----------------------------------------------------------------------------------------------

HANDLE WINAPI CreateThread(
  __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in       SIZE_T dwStackSize,
  __in       LPTHREAD_START_ROUTINE lpStartAddress,
  __in_opt   LPVOID lpParameter,
  __in       DWORD dwCreationFlags,
  __out_opt  LPDWORD lpThreadId
);
함수 인자 

----------------------

1. __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, 

-> 쓰레드가 생성되면 당연히 그에 대한 핸들이 생성된다. 이 인자에 NULL이 전달되면 핸들은 자식 프로세스 생성 시 상속 대상에서 제외된다.

----------------------

2. __in SIZE_T dwStackSize,

-> 쓰레드 생성시 해당 쓰레드를 위한 스택이 별도 생성된다는 것은 앞서 설명되었다. 0이 전달되면 이 쓰레드의 스텍 크기는 1M로 적용된다.

----------------------

3. __in LPTHREAD_START_ROUTINE lpStartAddress,

-> 쓰레드로 동작하기 위한 함수, 다시 말해서 쓰레드의 MAIN함수 역할을 하는 함수를 지정하는 전달 인자이다. 정확히 말하자면 함수의 포인터가 인자값으로 들어가야만 한다. 이 인자는 다음과 같이 재정의 되어 있다.

typedef DWORD (WINAPI *PTHREAD_START_ROUTINE) (LPVOID lpThreadParameter);

typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE; 

따라서 반환타입이 DWORD이고 매개변수 타임은 LPVOID(void*)인 형태로 함수가 정의 되어야 한다.

----------------------

4. __in_opt LPVOID lpParameter,

-> 쓰레드 함수에 전달할 인자를 지정하는 용도로 사용한다. main 함수에서 argv로 문자열이 전달되는 것과 유사하다.

---------------------- 5. __in DWORD dwCreationFlags,

쓰레드의 생성 및 실행을 조절하기 위한 전달인자. 각 플레그가 존재하며 몇가지 설명하자면.

CREATE_SUSPENDED가 전달되면, 쓰레드는 생성과 동시에 Blocked 상태에 놓이게 된다. 그러나 ResumeThread가 호출되면 실행을 시작한다.

XP이상의 윈도우 버전에서는 STACK_SIZE_PARAM_IS_A_RESERVATION을 전달 할수 있는데 이 경우 dwStackSize를 통해 전달되는 값의 크기는 reserve 메모리 크기를 의미하게 되고, 그렇지 않은 경우는 commit 메모리 크기를 의미하게 된다.

---------------------- 6. __out_opt LPDWORD lpThreadId

쓰레드 id를 전달받기 위한 변수의 주소값을 전달한다. 굳이 필요 없다면 NULL을 전달해도 좋다. Windows ME이하 버전에서는NULL을 전달 할 수 없기 때문에 범용적인 사용을 위해서라도 주소값을 전달하는게 좋다.

7. 함수 호출이 성공하면 생성된 쓰레드의 핸들이 반환된다.


쓰레드 생성에 있어서 주의할 사항은 쓰레드는 메모리가 허용하는 만큼만 생성이 가능하다. 쓰레드가 하나 생성될 때마다 독립된 스택을 할당해야 하므로 스택을 할당할 수 있을 때까지 쓰레드의 생성이 허용된다.


또한 이 함수로 생성된 쓰레드는 커널영역에서 스케줄러에 의해서 관리된다.


Posted by JJOREG

쓰레드의 진실 -----------------------------------------------------------------------------------------------------------------

프로세스가 실행될때 윈도우에서는 무조건 적으로 쓰레드를 하나 생성한다. 즉 프로세스A에 속한 쓰레드 A를 생성한다고 보면 된다.

그럼 컨텍스트 스위칭이란? 프로세스A에 속한 쓰레드 A와 프로세스B에 속한 쓰레드B간에 벌어지는 작업 즉 프로세스와 프로세스간에 존재하는 쓰레드를 의미한다.

그림으로 보면 아래와 같다고 할 수 있다.




커널 레벨 쓰레드와 유저레벨 쓰레드 ---------------------------------------------------------------------------------------------

유저 영역 -> 하나의 프로세스에 할당되는 메모리 공간이 존재할때 사용자에 의해서 할당되는 메모리 공간을 의미한다.

커널 영역 -> 하나의 프로세스에 할당되는 메모리 공간에서 당연히 운영체제또한 차지하는 메모리 공간이 있을 것이다. 운영체제라는 하나의 소프트 웨어를 실행시키기 위해서 필요한 메모리 공간을 커널영역이라고 한다. 

프로그래머가 개발한 프로세서A B C의 실행코드는 유저영역에 존재하지만 스케줄러나 쓰레드 정보는 커널영역에 존재한다. 오늘날의 대부분의 운영체제에서는 이러한 커널레벨을 기반으로 하여 쓰레드 모델을 지원한다.

만약 운영체제에서 커널레벨을 기반으로한 쓰레드를 지원하지 않거나 프로그래머가 커널레벨을 기반으로한 쓰레드를 사용하지 않을 경우 커널레벨에 의존적이지 않은 쓰레드 기능을 지원하는 라이브러리를 활용할수 있다. 이렇게 유저가 직접 쓰레드를 만드는 것을 유저레벨 쓰레드라고 한다.

즉 유저레벨 쓰레드와 커널레벨 쓰레드는 기능을 제공하는 주체가 누구냐에 따라서 달라진다.


32비트 CPU에서는 총 4기가의 메모리를 지원하고 그 영역을 절반으로 나누어 유저영역과 커널영역을 지정한다고 생각하면 된다.


커널 모드 유저 모드 ---------------------------------------------------------------------------------------------

운영체제는 커널영역의 보호를 위해서 커널 모드와 유저모드라는 것을 정의하고 있다. 일반적인 응용프로그램이 실행될 때 시스템은 유저모드 상태에 있다. 이 경우 제한된 영역의 메모 접근할수 있다. 그러나 커널모드에서는 전 영역의 메모리 접근이 가능하다. 따라서 커널레벨 쓰레드는 커널 모드에서만 동작한다.



Posted by JJOREG

멀티프로세스 기반 프로그램의 한계 -----------------------------------------------------------------------------

둘 이상의 서로 다른 프로그램을 실행하기 위해서는 당연히 둘 이상의 프로세스를 각각의 메모리영역을 메인메모리에 할당해야 한다. 또한 하나의 프로그램이 두가지 이상의 일을 동시(정확히는 동시가 아니지만)에 처리하기 위해서도 둘 이상의 프로세스가 필요하다.

하지만 이는 필연적으로 많은 횟수의 컨텍스트 스위칭(프로세스가 전환될 때마다 프로세스의 데이터를 CPU에 옮기는 작업)으로 인해서 성능 저하가 올수가 있기 때문이다.

그럼 이를 위한 해결책이 무엇일까? 단순하다. 컨텍스트 스위칭의 횟수를 줄이거나 정보의 개수를 줄이면 된다.


탄생 쓰레드! ------------------------------------------------------------------------------------------------

1. 쓰레드는 하나의 프로그램 내에서 여러개의 실행흐름을 두기 위한 모델이다.

2. 쓰레드는 각 실행흐름을 위한 메모리가 프로세스처럼 완벽한 독립구조가 아니다. 쓰레드들 사이에는 공유하는 요소가 존재한다.

3. 쓰레드는공유하는 요소가 있는 관계로 컨텍스트 스위칭에 걸리는 시간이 프로세스보다 짧다.



쓰레드의 특성! -----------------------------------------------------------------------------------------------

1. 쓰레드마다 스텍을 독립적으로 생성한다. 

-> 함수호출시 필요한 메모리 공간을 독립하면서 실행흐름을 추가시킬수 있는 최소한의 조건을 만족시킨다.

2. 코드 영역을 공유한다.

-> CPU는 당연히 필요한 종류의 명령어만 가지게 된다. 하지만 두개의 프로세스가 서로 다른 명령어를 가지고 있다면 당연히 프로세스 끼리는 각기 실행흐름을 실행 시킬 수 없다. 



하지만 쓰레드는 2개의 쓰레드에 있는 모든 명령어 종류를 공유함으로서 그 문제를 해결한다.




만약 MAIN프로세스가 있고 그 아래로 A쓰레드와 B쓰레드가 존재한다고 하면 각 쓰레드의 메인함수또한 마치 명령어 처럼 호출이 가능한 상태로 코드 영역에 등록된다.

3. 데이터 영역과 힙을 공유한다.


서로간의 데이터와 힙 영역을 공유하기 때문에 힙이나 데이터 영역에 메모리 공간을 할당해서 서로 통신하는 것이 가능하다. 

C++의 관점으로 본다면 전역변수와 NEW연산자를 통해서 동적 할당된 메모리 공간은 공유가 가능하다.

하지만 이것은 장점만 있는 것이 아니다. 동기화 문제를 통해서 프로그램에 오작동을 일으킬 여지도 도사리고 있다.

Posted by JJOREG
이전버튼 1 2 3 이전버튼