728x90

메모리의 범위와 종류

1.메인(Main) 메모리

메인 메모리인 램(RAM)은 정확하게 이야기 하자면 D램(D-RAM)계열의 메모리이다.

 

2.레지스터(Register)

CPU내부에 내장되어 있으며 연산을 위한 저장소의 역할을 한다.

 

3.캐쉬(Cache)

캐쉬는 D램보다 빠른 S램(S-RAM)으로 구성하는데 램이라는 단어는 이미 메인 메모리에서 사용되므로 그냥 캐쉬라고 표현한다. 캐쉬 메모리는 CPU에 가장 근접해 있는 메모리 이다.

(CPU 내부에 있는 것이 아니라 바로 옆에 있다는 것이다.)

 

4.하드 디스크(Hard Disk), 외의 저장 장치들

하드 디스크는 크고 작은 파일들을 저장하기 위한 용도로 사용되지만, 프로그램 실행에 있어서도 중요한 의미를 지닌다. 또한 SD카드, CD-ROM과 같은 I/O장치들도 메모리에 해당한다.

 

메모리 계층 구조

프로그램이 실행되는 동안 메모리의 역할은 데이터의 입력 및 출력이다.

 

메모리 사이의 가장 큰 차이점은 CPU로 부터의 거리이다.

레지스터는 CPU내부에 존재하는 메모리이다. CPU외부에서 가장 가까이 있는것이 캐쉬메모리이고, 그 다음이 메인메모리 이다. 가장 멀리있는 것이 하드디스크이다.

 

CPU와 가까이 있을 수록 속도가 빠르고, 멀리 있을 수록 속도가 느리다.

레지스터의 경우 CPU내부에 있으므로 접근에 있어서 별다른 절차가 필요없다. 그러나 메인 메모리에 접근하기 위해서는 몇몇 복잡한 과정을 거쳐야한다. 대표적인 것이 버스(BUS)인터페이스 컨트롤 이다. 데이터 입출력을 위해 버스를 거쳐야하기 때문에 그만큼 더 느리다.

 

하드디스크를 CPU가까이 가져간다면  속도도 빠르고 용량도 클것이다.

그러나 CPU근처로 대용량 메모리를 가져갈수록 기술적인 문제들도 발생하고 비용도 훨씬 많이 든다.

->하드디스크 용량을 수십기가바이트 늘리는 것보다 캐쉬 메모리 1M바이트 늘리는데 드는 비용이 훨씬 크다.

 

 

 

1.하드디스크에 있는 내용은 프로그램의 실행을 위해서 메인 메모리로 이동한다.

2.메인 메모리에 있는 데이터 일부도 실행을 위해서 L2캐쉬로 이동한다.

3.L2캐쉬에 있는 데이터 일부는 L1캐쉬로 이동을 하고, L1캐쉬에 있는 데이터 중에서 연산에 필요한 데이터가 레지스터로 이동한다.

 

 모든 메모리의 역할이 피라미드 구조에서 자신보다 아래에 있는 메모리를 캐쉬(자주 사용되는 메모리의 일부를 저장해서 속도를 향상시키는것)하기 위해서 존재한다.

 

1.연산에 필요한 데이터가 레지스터에 존재하지 않는다면 L1캐쉬를 확인한다.

2.L1캐쉬가 가지고 있지 않는다면 L2캐쉬를 확인한다.

3.L2캐쉬에도 없다면 메인메모리를 확인한다.

4.메인 메모리에도 존재하지 않는다면 결국은 하드디스크에서 읽어들이게 된다.

 

실제로는 메인메모리를 제외한 L1캐쉬와 L2캐쉬에, 연산에 필요한 데이터가 존재할 확률이 90%이상 된다. 캐쉬는 속도를 많이 향상시킨다.

 

Level1 캐쉬와 Level2 캐쉬

L1캐쉬는 CPU내부가 아니라 시스템 보드(메인 보드)에 존재하는 메모리 였지만, 성능향상을 위해 CPU내부로 들어가게 됐다. 그리고 CPU외부에 또 다른 캐쉬가 등장했는데 이것이 L2 캐쉬이다.

 

시스템의 성능을 좌우하는 클럭의 속도는 항상 느린쪽에 맞춰지게 되어있다.

예를들어 CPU는 상당히 고속화 되었지만, 메인메모리의 처리속도는 CPU의 속도를 따라가지 못하고있다. CPU속도가 두배 빨라져도 주변장치의 속도가 그대로 라면, 결코 기대하는 만큼의 속도 향상을 경험할 수 없다.

 

<메인 메모리 캐쉬간 병목현상>

ALU는 연산을 위해서 레지스터에 저장된 데이터를 가지고 와야하는데, 필요한 데이터가 존재하지 않을 경우 메인메모리에서 데이터를 읽어와야만 한다. 자주 사용되는 주소 번지의 데이터는 캐쉬 메모리에 저장해 두어 메인 메모리까지 가야만 하는 빈도수를 줄여준다.

 

필요로 하는 데이터가 캐쉬에 존재하지 않는다면 메인 메모리에서 데이터를 가져와야한다. 느린 메인메모리가 데이터를 전송해 줄 때까지 CPU는 쉬고 있어야만 한다. 여기가 '병목 현상'이 발생하는 부분이다.

 

병목현상을 최소화 하기 위해서는 캐쉬 사이즈를 키우면 된다. 그러나 이는 기술적으로 힘들기 때문에 캐쉬 메모리를 하나 더 설치한다.

 

L2 캐쉬의 역할

L2캐쉬의 개입으로 병목현상의 주체가 L1캐쉬에서 L2캐쉬로 이동하였다. L2캐쉬는 CPU 가까이 존재하므로 당연히 메인메모리보다 접근이 빠르다. CPU입장에서는 클럭속도의 차이에 의해 발생하는 병목현상의 부담을 그만큼 줄인 것이다.

 

병목현상의 발생위치가 L1에서 L2로 이동했을 뿐 CPU의 처리속도가 빨라졌다고 말할 수 있겠는가?

 

물론 빨라졌다. L2캐쉬는 L1캐쉬보다 크기가 크게 구성되기 때문에 기존의 L1캐쉬보다 필요로 하는 데이터를 훨씬 더 많이 저장할 수 있다. 그렇기 때문에 병목현상이 줄어들고 CPU의 처리속도가 향상되었다.

 

 

728x90
728x90

쓰레드의 생성과 소멸은 시스템에 많은 부담을 준다.

컴퓨터의 쓰레드 생성 및 소멸로 인해 성능이 저하되는 것을 피하기 위해 존재하는 것이 쓰레드 풀 이다.

쓰레드 풀의 기본 원리는 쓰레드 재활용이다.

할당된 일을 마친 쓰레드를 소멸시키지 않고, 쓰레드 풀에 저장해 뒀다가 필요할 때 다시 꺼내 쓰는 개념이다.

 

 

쓰레드 풀은 처리할 일(Work)이 등록되기 전에 생성된다.

풀이 생성됨과 동시에 쓰레드들도 생성되어 풀에서 대기하게 된다.

쓰레드 풀이 생성된 상태에서 처리해야 할 일(Work)이 하나 등록되었다면 쓰레드 풀에 존재하는 쓰레드 하나를 임의로 할당해서 일을 처리한다.

 

쓰레드 풀에 존재하는 쓰레드 수 보다 처리해야할 일의 수가 많다면, 일이 순서대로 처리되도록 디자인 할 수 있고, 추가적인 쓰레드가 생성되도록 풀을 디자인 할 수도 있다.

 

쓰레드 풀링은 한번 생성한 쓰레드를 재활용해서 시스템의 부담을 덜어주기 위한 기법이다.

쓰레드 풀은 여러개의 쓰레드를 생성한다. 그리고 실행해야 할 일이 등록될 때마다 미리 생성해 놓은 쓰레드 중 하나를 할당한다. 그리고 일이 끝나면 쓰레드는 소멸시키지 않고 다음 일을 위해서 보관한다.

728x90
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

+ Recent posts