728x90

Timer 기반 동기화 오브젝트는 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지닌다.

 

타이버 기반으로 동기화 하는 것은 임계영역 문제 해결을 위한 동기화가 아니라 쓰레드의 실행시간 및 실행 주기를 결정하겠다는 의미이다.

 

수동 리셋 타이머 : 가장 일반적인 타이머로, 알람시계와 같은 특성이다.

주기적 타이머 : 수동리셋 타이머에 주기적인 특성이 가해진 형태이다. 6시에 알람을 맞춰놓으면 6시에도 알람이 울리고 30분마다 한번씩 주기적으로 알람이 울리도록 설정하는 기능이 있다.

 

수동 리셋 타이머(Manual-Reset Timer)

타이머 오브젝트는 정해진 시간이 지나야 Signaled 상태가 되는 커널 오브젝트이다.

 

타이머 오브젝트 생성 함수

HANDLE CreateWaitableTimer(

    LPSECURITY_ATTRIBUTES IpTimerAttributes,

    BOOL bManualReset,

    LPCTSTR lpTimerName

);

1.lpTimerAttributes : 보안 속성 지정, 핸들을 자식 프로세스에게 상속하고자 할 경우 NULL이 아닌 다른 값

2.bManualReset : 타이머 오브젝트를 수동 리셋(Manual-Reset)모드로 생성 할 것인지, 자동 리셋(Auto-Reset)모드로 생성할 것인지 결정

3.lpTimerName : 타이머 오브젝트에 이름을 붙여줄 경우에 사용되는 전달 인자 NULL 전달 시 이름없는 타이머 오브젝트 생성

 

타이머 오브젝트는 무조건 Non-Signaled 상태로 생성된다.

시간이 지나서 Signaled 상태가 되어야 한다.

 

타이머 시간 설정 함수

BOOL SetWaitableTimer

   HANDLE hTimer,

   const LARGE_INTEGER * pDueTime,

   LONG lPeriod,

   PTIMERAPCROUTINE pfnCompletionRoutine,

   LPVOID lpArgToCompletionRoutine,

   BOOL fResume

);

1.hTimer : 알람을 설정할 타이머 오브젝트 핸들

2.pDueTime : 알람이 울리는 시간을 지정하는 매개변수, +값이 전달되면 절대 시간, -값이 전달되면 상대시간

"A시 B분 알람"-> +값

"지금으로부터 A초후 알람" ->? -값

1000만 분의 1초(100 Nanoseconds)단위로 시간 설정

3.lPeriod : 타이머가 주기적으로 알람을 울리게 할 때 사용하는 전달인자. 1/1000초(Milliseconds)단위로 전달한다. 0을 전달할 경우 주기적인 알람을 사용하지 않겠다는 의미이다.

4,5 pfnCompletionRoutine 과 lpArgToCompletionRoutine은 타이머를 생성하는 용도의 함수이다.

6.fResume은 전원관리와 관련이 있는 매개변수인데, 기본적으로 FALSE 전달을 원칙으로 한다.

 

LARGE_INTEGER는 64비트 정수를 표현하지 못하는 시스템에서 64비트 정수를 표현하기 위해 선언된 자료형이다.

 

주기적 타이머(Periodic-Timer)

SetWaitableTimer(

 hTimer, &liDueTime, 5000, NULL, NULL, FALSE);

위와 같이 3번째 인자에 5000을 넣으면 5초간격으로 타이머가 Signaled 상태가 된다.

 

CancleWaitableTimer 함수

가동중에 있는 타이머를 중지시키는 기능의 함수

타이머를 소멸시키거나 할당된 자원을 반환하는 종류가 아니다.

 

BOOL CancelWaitableTimer(

   HANDLE hTimer

);

1.hTimer : 알람을 해제할 타이머 오브젝트의 핸들을 전달한다.

전달된 타이머는 알람이 해제된다.

 

타이머의 자원을 반환하기 위해서는 CloseHandle을 사용해야한다.

 

 

 

 

 

 

728x90
728x90

쓰레드의 실행 순서를 동기화 한다는 것은 메모리에 접근하는 쓰레드의 실행 순서를 동기화 한다는 것이다. 즉 "실행 순서 동기화"는 "메모리 접근 동기화"를 포함하는 개념이다. 다만 초점이 실행 순서에 맞춰져 있고 이러한 부분을 부각시키기 위해 "실행 순서 동기화" 표현을 사용한다.

 

생산자 / 소비자 모델

생산자 / 소비자 모델은 실행되는 쓰레드의 순서가 중요한 상황을 설명할 때 종종 소개되는 모델이다.

생산자가 물건을 만들어야만 소비자가 소비할 수 있다. 이 순서가 바뀌어버린다면 소비자는 빈테이블에서 빵을 찾게되고, 소비자가 빈 테이블임을 알고 떠난 뒤에야 생산자가 빵을 가져다 놓는 일이 발생하게 된다. 당연히 이 빵은 소비되지 않고 남아있다.

 

이벤트(Event) 기반 동기화

세마포어나 뮤텍스와 같이 이 기법에서도 동기화를 위한 오브젝트가 사용된다. 이러한 오브젝트를 "이벤트(Event) 오브젝트"라고 표현한다. 그리고 이러한 오브젝트를 보편적으로 '이벤트'라고 부른다.

 

이벤트에는 이벤트를 생성 및 소멸시키는 함수와, 이벤트를 소유 및 반환하는 함수가 전부이다.

 

이벤트 오브젝트 생성 함수

HANDLE CreateEvent(

    LPSECURITY_ATTRIBUTES lpEventAttributes,

    BOOL bManualReset,

    BOOL bInitialState,

    LPCTSTR lpName

);

1.lpEventAttributes : 보안 속성을 지정할 때 싸용한다.

2.bManualReset : 수동 리셋(Manual-Reset)모드로 이벤트 오브젝트를 생성하느냐, 자동 리셋(Auto-Reset)모드로 이벤트 오브젝트를 생성하느냐를 결정짓는다. TRUE가 전달될 경우 수동 리셋(Manual-Reset) 모드 이벤트 오브젝트가, FALSE가 전달될 경우 자동 리셋(Auto - Reset) 모드 이벤트 오브젝트가 생성된다.

3.bInitialState : 이벤트 오브젝트의 초기 상태 결정, TRUE가 전달될 경우 Signaled 상태로 생성되고, FALSE 일 경우 Non-Signaled 상태의 이벤트가 생성된다.

4.lpName : 이벤트 오브젝트에 이름을 줄 경우에 사용하는 전달인자이다.

 

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

 

쓰레드나 프로세스의 커널 오브젝트의 경우, Non-Signaled 상태로 생성되고 쓰레드나 프로세스가 종료될 경우 해당 커널 오브젝트는 Signaled상태로 자동 변경된다.

이벤트 오브젝트의 경우 자동으로 Signaled 상태가 되는 상황이없다.

특정 함수 호출을 통해서 Signaled 상태로 변경해주어야 한다. 이벤트 오브젝트가 Signaled 상태가 되어 대기중이던 쓰레드가 블로킹 상태를 빠져 나왔을 때(WaitForSingleObject 함수를 호출 완료) 이벤트 오브젝트의 상태가 그대로Signaled 상태라면, 수동 리셋 모드(Manual-Reset 모드) 이벤트 오브젝트이고, 자동으로 Non-Signaled 상태로 변경됐다면, 자동 리셋 모드(Auto - Reset 모드) 이벤트 오브젝트이다.

 

벤트 오브젝트 실행순서 동기화

1.프로그래머 요청에 의해 이벤트는 Signaled 상태가 된다.

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

 

이벤트 오브젝트의 경우 자동 리셋 모드일때 WaitForSingleObject함수에 의해 자동으로 Signaled->Non-Signaled 로 변경된다.

 

이벤트 오브젝트의 상태를 변경시키는 함수

BOOL ResetEvent(

   HANDLE hEvent

);

1.hEvent : 이벤트 오브젝트의 핸들을 인자로 전달한다. 전달된 핸들의 오브젝트는 Non-Signaled 상태가 된다.

 

수동 리셋 모드 이벤트 동기화의 경우 동시에 여러 쓰레드가 접근이 가능하도록 만들 수 있다.

 

BOOL SetEvent(

   HANDLE hEvent

);

1.hEvent : 이벤트 오브젝트 핸들을 인자로 전달한다. 전달된 핸들의 오브젝트가 Signaled 상태가 된다.

728x90
728x90

이름있는 뮤텍스(Named Mutex)기반의 프로세스 동기화

뮤텍스나 세마포어를 생성할 때 오브젝트에 이름을 붙여줄 수 있다. 뮤텍스에 이름을 붙여 생성할 경우 "이름있는 뮤텍스(Named Mutex)"라 하고, 세마포어에 이름을 붙여 생성할 경우 "이름있는 세마포어(Named Semaphore)"라고 한다.

 

이름있는 뮤텍스 기반의 동기화는 서로 다른 프로세스 영역에 존재하는 쓰레드를 동기화 시키는데 사용된다.

 

뮤텍스는 커널오브젝트 이므로 모든 프로세스들이 접근할 수 있다. 그러나 뮤텍스의 핸들값은 뮤텍스를 생성한 프로세스만이 소유하고 있고 핸들값을 다른 프로세스에 넘겨줘도 핸들테이블에 등록되어있지 않아 의미가 없다. 핸들값에 대한 정보 즉 실제 커널오브젝트를 가리키는 핸들값은 핸들 테이블에 등록되어 있는데, 이 핸들 테이블은 프로세스 별로 독립적이다.

 

A프로세스에서 이름있는 뮤텍스를 생성 하고 실행중이다. 이후 B프로세스가 뮤텍스의 이름을 설정하고 OpenMutex함수를 호출하여 실행하면 A프로세스가 소유하고 있는 뮤텍스의 핸들을 얻을 수 있다.

B프로세스가 OpenMutex함수를 호출하여 뮤텍스를 소유할려고 하지만 A프로세스가 이미 소유하고 있기 때문에 A프로세스가 뮤텍스를 반환할 때까지 B프로세스는 실행을 멈춘다. A프로세스가 뮤텍스를 반환하면, B프로세스는 뮤텍스를 얻어서 실행하게 된다.

 

위와같은 원리로 이름있는 뮤텍스를 사용하면 서로 다른 프로세스에 존재하는 쓰레드 간에도 동기화가 가능하다.

 

OpenMutex 함수 선언

HANDLE OpenMutex(

    DWORD dwDesiredAccess,

    BOOL bInheritHandle,

    LPCTSTR lpName

);

1.dwDesiredAccess : 이름 있는 뮤텍스로의 접근 권한을 지정하는 것이다. 전달인자로 MUTEX_ALL_ACCESS을 전달해서 접근할 수 있는 권한을 요청해야 한다.

2.hInheritHandle : 핸들의 상속 유무를 결정하기 위한 전달인자 이다.

3.lpName : 얻고자 하는 핸들 정보의 커널 오브젝트 이름을 전달한다. 여기로 전달하는 이름과 일치하는 이름을 지니는 뮤텍스가 존재한다면, 이 뮤텍스의 핸들이 반환된다. 핸들 테이블에 이에 대한 정보도 추가된다.

 

 뮤텍스의 소유와 WAIT_ABANDONED

 

WaitForSingleObject 함수의 반환값중 WAIT_ABANDONED라는 것이 있다.

 

뮤텍스의 경우 획득한 쓰레드가 반환해야 하지만 세마포어는 그렇지 않다. 즉 세마포어의 경우 세마포어를 획득하는 쓰레드와 반환하는 쓰레드가 달라도 문제가 되지 않는다.

 

뮤텍스는 획득한 쓰레드가 직접 반환하는 것이 원칙이다. 본인만이 반환할 수 있다. 그러나 세마포어와 그 이외의 동기화 오브젝트는 마치 도서 대여점처럼 대신 다른 누군가가 반환해 줘도 문제가 되지 않는다.

 

쓰레드 A가 오브젝트인 뮤텍스를 소유하고 있다. 쓰레드B는 쓰레드 A가 뮤텍스를 반환하기를 기다린다. 그러나 예상치 못한 문제로 쓰레드 A가 뮤텍스를 반환하지도 않고 사라졌다. 이러한 경우 Windows는 정상적인 방법으로 반환이 불가능한 뮤텍스를 대신 반환해주고, 다음 대기자인 쓰레드 B가 뮤텍스를 소유할 수 있도록 도와준다. 이 때 쓰레드 B는 WAIT_ABANDONED값을 반환받게 된다.

728x90
728x90

세마포어(Semaphore)기반의 동기화

세마포어와 뮤텍스는 상당히 유사하다고 한다. 둘의 차이는 카운트(Count)기능이다. 세마포어는 카운트 기능이 존재하지만, 뮤텍스에는 존재하지 않는다.

뮤텍스에는 임계영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 없다. 그러나 세마포어는 임계영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 있다.

 

임계영역의 접근 허용 쓰레드 개수를 하나로 제한하기 위해 사용되는 세마포어를 가리켜 바이너리(Binary)세마포어 라고 한다. 바이너리 세마포어는 뮤텍스와 동일한 기능을 제공하게 된다.

 

세마포어 관련 함수

세마포어(세마포어 오브젝트)를 생성하는 함수

HANDLE CreateSemaphore(

     LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

     LONG lInitialCount,

     LONG lMaximumCount,

     LPCTSTR lpName

);

1.lpSemaphoreAttributes : 보안 속성을 지정하기 위한 매개변수이다.

2.lInitialCount : 임계영역에 접근 가능한 쓰레드의 개수를 제한한다. 세마포어에서 가장 중요한 전달인자이다.

3.lMaximumCount : 세마포어가 지닐수 있는 값의 최대 크기를 지정한다. 이 값이 1이면 뮤텍스와 동일한 기능을 하는 바이너리 세마포어가 구성된다. 기본적으로 lInitialCount 로 전달되는 값보다 커야한다.

4.lpName : 세마포어에 이름을 붙이기 위해 사용한다.

 

세마포어를 생성할 때 lInitialCount의 값을 10으로 설정한다면 WaitForSingleObject함수가 총 열번 호출될 때까지 카운트가 하나씩 감소하며 함수를 반환한다. WaitForSingleObject함수가 열한번쨰 호출될때 세마포어 카운트가 0인 관계로 블로킹 상태에 빠진다. 세마포어 카운터가 0보다 크면 Signaled 상태이고 0이라면 Non-Signaled 상태가 되는것이다.

 

임계영역을 빠져나온 쓰레드는 ReleaseSemaphore 함수를 호출해야 한다. 이 함수는 세마포어 카운트를 증가시키는 역할을 한다.

BOOL ReleaseSemaphore(

   HANDLE hSemaphore,

   LONG lReleaseCount,

   LPLONG lpPreviousCount

);

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

2.lReleaseCount : 증가시킬 값의 크기를 결정할 수 있다. 2를 전달할 경우 세마포어 카운트 2가 증가한다. 아주 특별한 경우가 아니라면 1을 전달하는 것으로 충분하다. 세마포어 생성시 결정한 최대 카운트 값(CreateSemaphore 함수 중 lMaximumCount)을 넘겨서 증가 시킬것을 요구하는 경우 카운트는 변경되지 않고 FALSE만 반환한다.

3.lpPreviousCount : 변경되기 전 세마포어 카운트 값(이 함수 호출을 통해서 증가하기 이전의 값)을 저장할 변수를 지정한다. 필요 없다면 NULL을 전달한다.

 

 

뮤텍스

뮤텍스에서는 뮤텍스락을 하나의 쓰레드만 얻을 수 있다. 즉, 하나의 쓰레드 만이 크리티컬 섹션에 접근가능하다.

 

세마포어

세마포어는 한순간에 크리티컬 섹션에 접근할 수 있는 쓰레드 개수를 지정해 줄 수 있다. 예를들어 동시에 3개의 쓰레드 접근하게 설정이 가능하다. 접근하는 쓰레드를 카운트 하는 기능이 세마포어에 있으며 ReleaseSemaphore함수는 이러한 카운트를 반환한다.

 

WaitForMultipleObject 함수가 관찰할 수 있는 최대 커널 오브젝트 수는 MAXIMUM_WAIT_OBJECTS(현재 64개)로 제한된다.

728x90
728x90

커널모드에서 동작하는 동기화 기법을 커널모드 동기화 기법이라고 한다.

커널모드 동기화는 유저모드 동기화에 비하면 느리다. 유저모드에서 커널모드로, 커널모드에서 유저모드로의 전환이 필요하기 때문이다. 그러나 커널모드 동기화 기법에서는 유저모드 동기화가 제공해주지 못하는 기능을 제공받을 수 있다.

 

뮤텍스(Mutex)기반의 동기화

뮤텍스 기반 동기화 기법의 경우 크리티컬 섹션 오브젝트와 같이 뮤텍스 오브젝트가 존재한다. 이는 크리티컬 섹션 오브젝트와 달리 다음 함수를 통해서 만들어 진다.

HANDLE CreateMutex(

    LPSECURITY_ATTRIBUTES lpMutexAttributes,

    BOOL bInitialOwner,

    LPCTSTR  lpName

);

1.lpMutexAttributes : 뮤텍스도 커널오브젝트 이기 때문에 이 값을 통해 보안속성을 지정해 줄 수 있다.

2.bInitialOwner : 뮤텍스는 뮤텍스 오브젝트를 생성하는 쓰레드에게 임계영역에 접근하는 기회를 먼저 줄 수 있다. FALSE를 전달할 경우 크리티컬 섹션처럼 먼저 접근하는 쓰레드가 임자가 되게할 수 있다. TRUE를 전달할 경우 뮤텍스를 생성하는 쓰레드가 먼저 기회를 얻을 수도 있다. 이를 결정하는 전달인자이다.

3.lpName : 뮤텍스에 이름을 붙여주기 위해 사용한다. 이름은 널(NULL)문자로 끝나는 문자열로 저장하면 된다. 이름을 주었을 때 생성되는 뮤텍스를 가리켜 Named Mutex(이름있는 뮤텍스)라 표현한다.

4.함수의 반환타입 HANDLE : 반환 타입이 HANDLE이라는 것은 뮤텍스가 커널 오브젝트임을 말하는 것이다.

 

뮤텍스가 커널 오브젝트인 점만 보더라도, 뮤텍스는 커널레벨 동기화 기법임을 알 수 있다. 뮤텍스는 함수 호출 과정에서 필요한 모든 초기화가 이루어 진다.

 

 "커널 오브젝트는 상태를 지닌다. 하나는 Signaled 상태이고, 다른 하나는 Non-Signaled 상태이다."

보통 커널오브젝트는 Non-Signaled 상태에 놓여있다가, 특정 상황이되면 Signaled 상태가 된다.

 

뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓이고 누군가에 의해 획득되어져 있는 상태라면 Non-Signaled 상태가 된다.

 

뮤텍스는 획득이 가능할 때 Signaled 상태에 놓인다. 따라서 WaitForSingledObject 함수를 임계영역 진입을 위한 뮤텍스 획득의 용도로 사용가능하다.

 

뮤텍스를 반환할 때 사용하는 함수(뮤텍스를 Signaled 상태로 설정)

BOOL ReleaseMutex(

   HANDLE hMutex

);

1.hMutex : 반환할 뮤텍스 핸들을 인자로 전달한다. Non-Signaled 상태에 있는 뮤텍스 오브젝트는 Signaled 상태가 된다.

 

WaitForSingleObject 함수의 특성 : WaitForSingleObject 함수는 인자로 전달된 핸들의 커널오브젝트가 Signaled상태가 되어서 반환하는 경우, 해당 커널 오브젝트를 Non-Signaled 상태로 변경해 버린다.

 

뮤텍스 동기화는 ReleaseMutex 함수와 WaitForSingleObject 함수로 조절 가능하다.

 

쓰레드는 임계영역에 들어가기에 앞서 뮤텍스를 획득해야한다. 따라서 뮤텍스 핸들을 인자로 전달하면서 WaitForSingleObject함수를 호출한다. 뮤텍스가 획득가능한 상태라면 Signaled 상태일 것이다. 때문에 뮤텍스를 획득하면서 임계영역에 진입하게 된다. WaitForSingleObject함수는 커널오브젝트가 Signaled 상태가 되어 반환할 경우, 해당 커널오브젝트 상태를 Non-Signaled 상태로 변경하므로, 다른 쓰레드들은 임계영역으로의 진입이 제한된다. 임계영역에서의 일을 마친 쓰레드가 임계영역을 빠져 나오면서 ReleaseMutex함수를 호출한다. 이 함수가 호출되면, 뮤텍스는 다른 누군가에게 획득이 가능한 상태, 즉 Signaled 상태가 되어서 다른 쓰레드의 진입을 허용한다.

 

뮤텍스는 커널 오브젝트이다. 해당 리소스를 제거할 때는 CloseHandle 함수를 통해 반환하면 된다. 소멸은 운영체제가 해준다.

728x90
728x90

유저모드 도기화 기법을 적용할 경우, 커널 모드로의 전환이 불필요 하기 때문에 성능상 이점을 얻을 수 있다. 그리고 커널 모드 동기화에 비해 활용하는 방법도 단순하다.

 

크리티컬 섹션(Critical Section) 기반의 동기화

크리티컬 섹션 기반의 동기화를 사용하려면 크리티컬 섹션 오브젝트를 만들고 초기화 해야한다.

크리티컬 섹션 오브젝트는 자료형 CRITICAL_SECTION의 변수이다.

선언 예시)

CRITICAL_SECTIN gCriticalSection;

 

크리티컬 섹션 오브젝트를 선언한 이후에는 다음 함수를 통해 초기화 과정을 거쳐야 한다.

void InitializeCriticalSection(

   LPCRITICAL_SECTION lpCriticalSection

);    // lpCriticalSection : 초기화 하고자 하는 크리티컬 섹션 오브젝트의 주소를 넘긴다.

위 함수로 초기화 과정을 거쳐야만 크리티컬 섹션 오브젝트는 사용가능한 상태가 된다.

 

크리티컬 섹션 오브젝트 초기화 과정

ex)

CRITICAL_SECTION * gCriticalSection;

 

int _tmain(int argc, TCHAR * argv[])

{

    ...

    InitializeCriticalSection(gCriticalSection);

    ...

}

 

크리티컬 섹션에 진입하고 빠져나오는데에 특별한 함수가 필요하다. 크리티컬 섹션 접근 동기화를 위한 함수들이다.

 

크리티컬 섹션 접근함수

void EnterCriticalSection(

   LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 임계 영역에 진입하기 위해 필요한 크리티컬 섹션 오브젝트의 주소값이 인자로 사용된다. 다른 쓰레드에 의해 이 함수가 호출된 상태라면 호출된 함수는 블로킹 상태가 되고 다른 쓰레드가 반환하면 호출된 함수는 블로킹 상태를 빠져나온다. 위 함수를 호출한 쓰레드가 크리티컬 섹션 오브젝트를 획득했다고 표현한다.

 

크리티컬 섹션 빠져나오는 함수

void LeaveCriticalSection(

  LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 위 함수는 임계영역을 빠져 나와서 호출하는 함수이다. 다른 쓰레드가 EnterCriticalSection 함수를 호출하고 블로킹 상태에 놓여있다면, LeaveCriticalSection 함수 호출을 통해 현재 쓰레드가 임계영역 접근을 반환하고 블로킹 상태에 놓여있던 다른 쓰레드가 블로킹 상태를 빠져나와 임계영역에 접근한다.

LeaveCriticalSection 함수가 호출이 완료되었을 때 이 함수를 호출한 쓰레드가 크리티컬섹션 오브젝트를 반환했다고 표현한다.

 

 위 두 함수 사용 형태

//임계 영역 진입을 위한 크리티컬 섹션 오브젝트 획득

EnterCriticalSection(&CriticalSection);

                ...

            임계영역

                ...

//크리티컬 섹션 오브젝트 반환(임계영역 빠져나옴)

LeaveCriticalSection(&CriticalSection);

 

임계영역이 결정되면 임계영역 진입 이전에 "EnterCriticalSection" 함수를 호출하고, 임계영역을 빠져나간 이후에 "LeaveCriticalSection"함수 호출로 크리티컬 섹션 오브젝트를 반환한다. 이러한 방법을 통해 임계영역에 한순간에 하나의 쓰레드만 실행할 수 있도록 구성하는 것이 크리티컬 섹션 동기화 기법의 핵심이다.

 

크리티컬 섹션 오브젝트 반환 함수

void DeleteCriticalSection(

    LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 반환하고자 하는 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.

위함수를 통해 크리티컬 섹션 오브젝트를 제거한다.

 

인터락 함수(Interlocked Family Of Function)기반의 동기화

인터락 함수는 내부적으로 한순간에 하나의 쓰레드에 의해서만 실행되도록 동기화 되어있다. 인터락 함수는 전역으로 선언된 변수 하나의 접근 방식을 동기화 하는 것에 특화 되어 있다.

 

인터락 함수

LONG InterlockedIncrement(

    LONG volatile* Addend

);

Addend : 값을 하나 증가시킬 32비트 변수의 주소값을 전달한다. 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 증가시킬 경우 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받을 수 있다.

 

LONG InterlockedDecrement(

  LONG volatile * Addend

);

Addend : 값을 하나 감소시킬 32비트 변수의 주속ㅄ을 인자로 전달한다. 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해서 감소 시킬 경우, 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받을 수 있다.

 

InterlockedIncrement 함수와 InterlockedDecrement 함수는 원자적 접근(Atomic Access), 한순간에 하나의 쓰레드만 접근하는것을 보장해주는 함수이다. 크리티컬 섹션 동기화 기법도 내부적으로는 인터락 함수를 기반으로 구현되어있다. 위의 인터락 함수들도 유저모드 기반으로 동작하기 때문에 속도가 상당히 빠르다.

위 함수들을 통해 더 간결하게 안전한 쓰렏 형태를 만들 수 있다.

ex)

void IncreaseCount()

{

  //gTotalCount++;

  InterlockedIncrement(&gTotalCount+);

}

 

마이크로 소프트에서는 더 다양한 인터락 함수들을 제공한다.

원하는 수만큼 값을 증가시키거나 감소시키는 함수, 64비트 변수를 대상으로 연산하는 함수 등등

 

volatile 키워드

volatile 키워드의 의미는 크게 두가지가 있다.

1.최적화를 수행하지 마라

2.메모리에 직접 연산하라

 

1.최적화를 수행하지 마라

컴파일러는 코드를 컴파일 하는 과정에서 코드의 최적화를 수행한다.

ex)

int function(void)

{

  int a=10;

  a= 20;

  a= 30;

 cout << a;

}

--->

int Function(void)

{

     int a= 30;

     cout << a;

}

프로그램  실행결과가 동일하다는 관점에서 컴파일 과정에서의 코드 최적화가 이루어 질수도 있다.

이러한 최적화가 문제가되는 상황이 존재한다.

임베디드 시스템을 구성할 때 메모리 맵(Memory Map)디자인 이라고 하는 과정을 거치게 되는데, 이 과정에서 출력을 위해 사용되는 LCD나 소리를 내기 위해 사용되는 오디오 칩과 같은 하드웨어 장치에도 주소를 할당하게 된다. 즉 메모리 주소가 RAM과 같은 저장장치에만 할당되는 것이아니라, 하드웨어 장치에도 할당된다.

 

위와같은 상황에 최적화를 막기 위해 volatile 키워드를 사용한다.

ex)

int function(void)

{

   int volatile  * psound = 0x30000;

   ...

}

 

2."메모리에 직접 연산하라!"

ex)

int function(void)

{

   int * psound = 0x30000;

   SleepUntil(3, 35, 12);

   *psound = 2;

   ...

}

SleepUntil 이라는 함수를 만들어서 사용한 프로그램이다. 위 프로그램을 3시 35분 12초에 "미"음을 내는 프로그램이다.

3시35분 12초에 "* psound = 2"즉 0x30000에 데이터 2가 입력되면서 '미'음이 발생해야 한다. 그러나 성능향상을 위해 캐쉬메니저가 데이터를 캐쉬 메모리에 저장했다면 우리가 원하는 시점에 '미'음을 들을 수 없다.

(언젠가는 캐쉬에 저장된 데이터가 메모리에 저장되므로 소리는 발생할 것이다.)

 

위와같은 문제를 막기위해 volatile키어드를 사용할 수 있다. volatile로 선언되면 해당 데이터는 절대로 캐쉬되지 않는다. 바로 메모리에 직접 연산하게된다.

 

인터락 함수 인자들이 volatile로  선언되어 있다. 이는 함수 내부적으로 최적화를 수행하지 않으며, 해당 포인터가 가리키는 메모리영역을 캐쉬하지 않겠다는 것을 의미한다.

728x90
728x90

쓰레드 동기화는 일치한다는 의미에서의 동기화가 아니라 순서에 있어서 질서가 지켜지고 있음을 의미한다. 즉 쓰레드동기화는 쓰레드의 실행순서를 정의하고, 이 순서에 반드시 따르도록 하는 것이 쓰레드 동기화 이다.

또한 한순간에 하나의 쓰레드만 접근해야 하는 메모리 영역이 존재한다. (데이터 영역, 힙 영역) 메모리 접근에 있어서 동시 접근을 막는것 또한 쓰레드의 동기화에 해당한다.

"실행 순서의 동기화"와 "메모리 접근의 동기화"가 있다.

 

Windows에서의 동기화 기법

Windows에서의 동기화 기법은 제공하는 주체에 따라 두가지로 나뉜다.

1.유저모드 동기화(User Mode Synchronize) 기법

2. 커널 모드 동기화(Kernel Mode Synchronize)기법 이 있다.

 

유저 모드 동기화 : 동기화 과정에서 커널코드가 실행되지 않는 동기화 기법이다. 동기화 과정에서 커널 모드로의 전환이 없어 성능상 좋다.

 

커널 모드 동기화 : 커널에서 제공하는 동기화 기능을 활용하는 방법이다. 동기화 과정에서 커널 함수를 사용하여 커널모드로의 전환이 필요하고, 성능 저하가 발생한다. 그러나 유저 모드 동기화에서 제공 못하는 기능을 제공받을 수 있다.

 

1.유저 모드 동기화

1-1 크리티컬 섹션(Critical Section) 기반의 동기화

   - 메모리 접근 동기화에 사용 예정

1-2 인터락 함수 (Interlocked Family Of Function)기반의 동기화

   - 메모리 접근 동기화에 사용 예정

 

2.커널 모드 동기화

2-1 뮤텍스(Mutex) 기반의 동기화

   - 메모리 접근 동기화 사용할 예정

2-2 세마포어(Semaphore) 기반의 동기화

   - 메모리 접근 동기화에 사용할 예정

2-3 이름 있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화

   - 프로세스간 동기화에 사용할 예정

2-4 이벤트(Event) 기반의 동기화

   - 실행 순서 동기화에 사용할 예정

728x90
728x90

쓰레드의 상태는 계속해서 변경된다. 입력 및 출력 연산을 시작하거나 종료하는 경우, 또는 새로운 쓰레드 생성에 의해서도 변경될 수 있다. 상황이나 운영체제 관리방법에 따라 쓰레드의 상태가 변경되므로 프로그래머가 상태를 직접적으로 컨트롤 하는것이 아니다.

 

그러나 경우에 따라서는 쓰레드의 상태를  프로그래머가 임의로 변경시켜야만 하는 경우도 있을 수 있다. 특정 쓰레드의 상태를 Blocked 상태로 만든다거나, Ready상태로 둔다거나 경우에 따라서 상태를 변경시키는것이 필요할 수 있다.

 

쓰레드의 상태변화

Windows에서는 상태가 변화하는 주체가 프로세스가 아니라 쓰레드이다.

1,2

쓰레드는 생성되면 Ready상태에 놓이게 된다. 이후 스케줄러에 의해서 선택될 경우 Running 상태가 되어 실제 실행이 된다. Ready상태에 놓이는 쓰레드는 여러개일 수 있지만, Running 상태 즉 현재 실행중인 쓰레드는 하나밖에 될 수 없다.

 

3.

실행중인 쓰레드에게 할당된 타임 슬라이스(Time Slice)가 모두 소비되어서, 다른 쓰레드에게 실행기회를 넘겨야 할 때, Running 상태에서 Ready상태로의 이동이 이뤄진다. Ready상태로 이동시켜야만 언제든지 다시 실행될 수 있기 때문에 Ready상태로 이동하는 것이 맞다.

 

4,5

Running 상태에 있는 쓰레드가 입,출력 연산을 하거나, Sleep함수가 호출될 경우 Blocked 상태로 이동하고 다른 쓰레드의 실행을 도모하게 된다. Block 상태가 끝나면 다시 Ready 상태로 돌아가서 실행을 기다린다.

 

Suspend & Resume

쓰레드의 상태 컨트롤

DWORD SuspendThread(

     HANDLE hThread

);

Blocked 상태에 두고자 하는 쓰레드의 핸들을 인자로 전달한다.

 

DWORD ResumeThread(

    HANDLE hThread

);

Ready 상태에 두고자 하는 쓰레드의 핸들을 인자로 전달한다.

 

쓰레드의 커널 오브젝트에는 SuspendThread 함수의 호출 빈도수를 기록하기 위한 서스펜드 카운트(Suspend Count)라 불리는 멤버가 존재한다. 현재 실행중인 쓰레드의 서스펜드 카운트는 0이다.

이 쓰레드 핸들을 인자로 SuspendThread 함수가 호출이 되면, 서스펜드 카운트는 1이되고 쓰레드는 Blocked 상태가 된다. 이 상태에서 SuspendThread함수를 한번더 호출하면 서스펜드 카운트는 2가된다. 즉 SuspendThread 함수는 서스펜드 카운트를 1증가시킨다. ResumeThread함수는 반대로 서스펜드 카운트를 하나 감소시키는 역할을 한다. 즉 위 상황에서는 ResumeThread함수를 두번 호출해야 서스펜드 카운트가 0이되고 Ready상태에 놓이게 된다.

 

SuspendThread 함수와 ResumeThread 함수의 반환값은 모두 변경되기 이전의 서스펜드 카운트를 반환한다.

 

CreateThread의 인자로 CREATE_SUSPENDED가 전달되면, 쓰레드는 생성되자 마자 서스펜드 카운트가 1이다. 즉 쓰레드가 생성되자 마자 Blocked상태가 되는 것이다.

 

쓰레드의 우선순위 컨트롤

 프로세스는 쓰레드를 담는 그릇이다. Window에서는 프로세스가 아닌 프로세스 내부에서 동작하는 쓰레드가 우선순위를 갖는다.

 

쓰레드의 상대적 우선순위           Priority                                                       Meaning

THREAD_PRIORITY_LOWEST                                                                        -2

THREAD_PRIORITY_BELOW_NORMAL                                                            -1

THREAD_PRIORITY_NORMAL                                                                       0(Default)

THREAD_PRIORITY_ABOVE_NORMAL                                                             +1

THREAD_PRIORITY_HIGHEST                                                                        +2

 

 쓰레드의 우선순위는 프로세스의 우선순위와 쓰레드의 우선순위 조합으로 결정된다.

예를 들어 우선순위가 NORMAL_PRIORITY_CLASS(9)인 프로세스 안에 두개의 쓰레드가 존재한다. 각각 쓰레드 우선순위가 THREAD_PRIORITY_LOWEST(-2), THREAD_PRIORITY_NORMAL(0)이라면각쓰레드의우선순위는7(9-2),9(9-0)으로결정된다.

즉 프로세스 우선순위에서 쓰레드 우선순위에 해당하는 값을 더하거나 빼면 쓰레드의 실질적인 우선순위가 나온다.

 

프로세스 내에서 생성되는 모든 쓰레드의 우선순위는 THREAD_PRIORITY_NORMAL이다. 즉 프로세스의 기존 우선순위를 그대로 수용하는 것이다. 이를 변경하거나 참조할때 다음 두 함수를 사용한다.

BOOL SetThreadPriority(

     HANDLE hThread,

     int     nPriority

);

 

int GetThreadPriority(

     HANDLE    hThread

);

728x90

+ Recent posts