쓰레드 동기화란? ---------------------------------------------------------------------------------------------
앞서 설명되었던 쓰레드간의 실행순서의 문제를 해결하기 위한 방법을 의미한다.
쓰레드 동기화는 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)할 때 사용할 수 있다.
즉 컴파일러의 최적화가 항상 옳지는 않고 프로그래머의 의도와 컴파일러의 판단은 같지 않을수 있다는 것을 보여준다.
'게임개발공부 > 서적공부' 카테고리의 다른 글
윈도우 시스템 프로그래밍 <커널모드 동기화> #실행순서 동기화 (0) | 2013.11.21 |
---|---|
윈도우 시스템 프로그래밍 <커널모드 동기화 방식 1> (0) | 2013.11.17 |
윈도우 시스템 프로그래밍 <쓰레드의 문제점과 상태> (0) | 2013.11.16 |
윈도우 시스템 프로그래밍 <쓰레드 소멸> (0) | 2013.11.16 |
윈도우 시스템 프로그래밍 <쓰레드 생성> (0) | 2013.11.16 |