2010년 12월 5일 일요일

Project H3

 

H3 동영상이 공개가 되었다..

 

참 열악한 상황해서 개발했는데.. 아둥바둥 노력해서 모양세가 나오는것 보니

참 눈물이 앞을 가린다.

 

지금은 자리를 옮겼지만 한때 나의 노력이 녹아있기 때문에 잘되었으면 하는 바램이다.

2010년 12월 3일 금요일

스레드 동기화

펌) http://kslive.tistory.com/23

 

◎ 동기화의 필요성

 다수의 스레드가 하나의 데이터를 동시에 엑세스를 하는 경우는 우리가 스레드 프로그래밍을 하는 동안에는 피할 수 없는 현상이다. 다수의 스레드가 하나의 데이터를 읽기만 한다면야 이 데이터에대한 동기화의 필요성은 필요하지 않을 것이다. 왜냐하면 읽기만 한다는건 데이터가 변하지 않는다는것을 말하기때문이다. 그러므로 어느 스레드에서든 해당 데이터의 값은 동일할 것이다. 하지만 다수의 스레드가 하나의 데이터에 대한 읽기와 쓰기의 작업을 동시에 한다면 스레드의 동기화를 사용하지 않는다면 이 데이터에 대한 무결성을 입증할 방법이 존재할 수 있을까? 답은 없다이다. CPU는 한줄의 대입 연산을 실행하는 경우에도 그에 해당하는 여러줄의 기계어 코드를 만들기때문에 원자적 접근이 되지않기 때문이다.


 ◎ 원자적 접근

 스레드 동기화는 원자적 접근을 보장해주는 일이다. 원자접근은 같은 시간에 같은 리소스에 다른 스레드가 접근하지 않도록 하면서 해당 스레드가 리소스에 접근하도록 하는 일일 말한다.


 ◎ Interlocked 계열의 함수

 Interlocked 계열의 함수는 하나의 long형 변수의 값을 변경할때 원자접근을 수행하는 함수를 말한다. Interlocked 계열의 함수는 다음과 같이 존재한다. (현재는 더 존재할 수 있다.)


. LONG InterlockedIncrement (LONG volatile* Addend) : Addend 에 저장되어있는 long형 변수를 1 증가 시킨다. Rv는 증가된 후의 값이다.


. LONG InterlockedDecrement(LONG volatile* Addend) : Addend 에 저장되어있는 long형 변수를 1 감소 시킨다. Rv는 감소된 후의  값이다


. LONG InterlockedExchage(LONG volatile* Target, LONG Value) : Target에 저장되어있는값을 Value값으로 변경한다. Rv는 최초 Target에 저장되어있는 값이다.


.PVOID InterlockedExchangePointer(PVOID volatile* Target, PVOID Value) : Target에 저장되어있는 값을 Value에 저장되었는 값으로 변경한다. Rv는 최초 Target에 저장되어있는 값이 있는 번지를 리턴한다.


.LONG InterlockedExchangeAdd(LONG volatile* Target, LONG Increment) : Target에 저장되어있는값에 Increment값을 더한다. Rv는 최초 Target에 저장되어있는 값이다.


.LONG InterlockedCompareExchange(LONG volatile* Dest, LONG Exchange, LONG Compare) : Dest에 저장되어있는값이 Compare와 동일하면 Dest의 값을 Exchange으로 변경한다. Rv는 최초 Dest에 저장되어있는 값이다.


.PVOID InterlockedCompareExchangePointer(PVOID* pDest, PVOID pExch, PVOID pCompare) : pDdest에 저장되어있는 값이 pCompare와 동일하면 pDest의값은 pExch값으로 변경된다. Rv는 최초 pDest의 값이다.


 ◎ CRITICAL_SECTION

 CRITICAL_SECTION 은 윈도우에서 제공하는 동기화 객체 중의 하나로서 동기화 객체 중 유일하게 유저모드에서 실행된다.(커널모드로의 전환이 일어나면 CPU사이클을 많이 잡아먹는다. 한마디로 느리다는것이다.)  

  CRITICAL_SECTION의 사용법은 다음과 같다.


1.  CRITICAL_SECTION 구조체를 초기화 한다.


void InitializeCriticalSection(LPCRITICAL_SECTION pCs);


사용할 CRITICAL_SECTION 객체의 주소를 넘겨준다.


2. 임계영역에 진입한다.


VOID EnterCriticalSection(LPCRITICAL_SECTION pCs);


3. 해당작업을 한다. (원자성을 보장한다.)


4. 임계영역을 탈출한다


void LeaveCriticalSection(LPCRITICAL_SECTION pCs);


 ※ 인자로 들어가는 CRITICAL_SECTION 구조체의 주소가 동일해야한다.

 ※ 3번에서 원자성을 보장한다는 얘기는 해당작업을 하는 부분에서 사용하는 공통된 데이터에 접근 하는 모든 스레드에서 EnterCriticalSection와 LeaveCriticalSection를 사용해야 하며, 인자로 넘어가는 CRITICAL_SECTION 의 구조체의 주소는 모두 동일해야 한다.


위의 순서에서 사용된 함수들에 대한 설명은 다음과 같다.


void InitializeCriticalSection(LPCRITICAL_SECTION pCs)

 : CRITICAL_SECTION 구조체를 초기화한다. 이 함수는 단지 멤버변수를 설정하기 때문에 실패하지 않는다. 이 함수는 EnterCriticalSection을 호출하기전에 반드시 호출되어야한다. 초기화되지않은 CRITICAL_SECTION 에 진입을 시도할시 결과는 정의되지 않는다고 SDK문서에 명시되어있다.

※ 이 함수가 실패는 하지 않지만 예외상황을 발생시킬수있다. 위 함수는 디버깅 정보가 있는 메모리 블럭을 할당하기 때문에 메모리 블럭 할당에 실패하였을 경우 STATUS_NO_MEMORY 예외를 던진다. SEH를 이용하여 예외를 Catch할수있다.


.void DeleteCriticalSection(LPCRITICAL_SECTION pCs)

 : DeleteCriticalSection은 구조체 내의 멤버 변수를 재설정한다(제거). 어떤 스레드가 여전히 이 구조체를 사용하고있다면 자연히 임계영역은 삭제할 수 없다. SDK 문서에서는 이렇게 할때 결과는 정의되지 않는다고 나와있다.


.VOID EnterCriticalSection(LPCRITICAL_SECTION pCs)

 : 임계영역에 진입을 시도한다.

 ※ 한 CRITICAL_SECTION 에 대해 두번의 EnterCriticalSection은 시도할경우 최초 진입 성공시 두번째 EnterCriticalSection시에는 CRITICAL_SECTION의 접근 스레드를 표시하는 변수를 업데이트하고 바로 리턴한다. (Enter가 두번이면 Leave도 두번호출해야한다.)


BOOL TryEnterCriticalSection(LPCRITICAL_SECTION pCs)

 : 임계영역에 진입을 시도한다. 이 함수는 이함수를 호출하는 스레드가 절대 대기상태로 진입을 하지 않는다. 이 함수가 나타내는 Rv값은 다른 스레드에서 해당 임계영역에 진입을 하고있는 상태면 FALSE, 아니면 TRUE이다. 이 함수는 해당 임계영역에 진입이 가능하면 진입을 시도하고 아닐경우 바로 리턴하여(FALSE) 다른 일을 한다.

※ Windows 2000 이상에서만 지원한다.


.void LeaveCriticalSection(LPCRITICAL_SECTION pCs)

 : 이 함수는 호출한 CRITICAL_SECTION에 접근하고있는 스레드의 개수를 하나 감소시킨다. 접근스레드의 개수가 1이상이면 감소 후 바로 리턴하고.개수가 0개이면EnterCriticalSection을 호출한 다른 스레드가 대기 상태에 있는지 검사 후 존재하면 해당 스레드에게 맞게 CRITICAL_SECTION 구조체를 업데이트하고 바로 해당스레드를 스케쥴한다.(여러개가 존재할경우 공정하게 선택한단다.-_-;;) 대기하는 스레드가 없을 경우 멤버함수를 업데이트하여 접근하는 스레드가 없음을 나타낸다.


 ◎ 스핀록

 스레드가 EnterCriticalSection을 호출했을 시 해당 임계영역을 다른 스레드가 소유하고있으면 호출한 스레드는 바로 대기모드로 전환한다. 이는 유저모드에서 커널모드로의 전환을 의미한다.(CPU사이클을 많이 소모한다.) 이 전환은 소모가 매우 크다. 그래서 MS는 이 비용을 줄이기 위해 임계 영역안에 스핀록을 만들었다. EnterCriticalSection을 호출했을시 EnterCriticalSection은 리소스를 몇번 동안 요청을 시도하기위해 시핀록을 사용해 루프를 돈다. 오직 모든 시도가 실패하게 될때 스레드는 커널모드로(대기모드) 전환된다.


스핀록을 사용하기위해서는 다음과 같은 함수를 사용한다.


BOOL InitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTION pCs, DWORD dwSpinCount);

: InitliazeCriticalSection을 사용하지 말고 위 함수를 사용하여 CRITICAL_SECTION을 초기화 해야한다. dwSpincount는 리소스 재요청 횟수이다. 위 횟수는 0~ 0x00FFFFFF 사이의 어떤값도 될수 있다. InitliazeCriticalSection 은 항상 성공하며 가끔 예외(?)를 던지지만, 이 함수는 메모리 할당에 실패하면  FALSE를 리턴한다. 훨씬 좋다. 사용하자.

 ※ 프로세서가 하나인 머신에서는 dwSpinCount무시되고 항상 0 으로 설정된다.


DWORD SetCriticalSectionSpinCount(LPCRITICAL_SECTION pCs, DWORD dwSpinCount)

 : 위 함수는 해당 CRITICAL_SECTION객체의 스핀횟수를 변경한다.

 ※ 역시 프로세서가 하나인 머신에서는 위의  dwSpinCount값은 무시되고 0으로 설정된다.

※ 임계영역에서 스핀록을 사용하면 손해보지는 않는다. 지원을 하면 항상 사용하자.


 ◎ 임계영역 & 에러 핸들링

내부적으로 임계 영역은 두개 이상의 스레드가 동시에 임계 영역을 가지고 경쟁할 경우 이벤트 커널 오브젝트를 사용한다. 이런 경쟁이 드물면 시스템은 이벤트 커널 오브젝트를 생성하지 않는다. 메모리가 적은 상황에서 임계 영역에대한 경쟁 상황이 발생할 수 있고 시스템은 요청된 이벤트 커널 오브젝트의 생성에 실패하게 되면  EnterCriticalSection함수는 EXCEPTION_INVALID_HANDLE 예외를 발생한다. 이런 예외에 대한 처리는 대부분 하지 않기 때문에 치명적인 상황이 발생할 수 있다.


 위의 상황을 방지하기 위해서는 SEH로 예외를 핸들링하는방법과(비추이다.)

InitializeCriticalSectionAndSpinCount을 사용해서 임계영역을 생성하는것이다. (dwSpincount를 높게 설정해서) dwSpinCount가 높게 설정되면 이 함수는 이벤트 커널 오브젝트를 생성하여 이것을 임계영역과 연결한다. 만약 높은 비트로 설정이 되었는데 이벤트 커널 오브젝트가 생성되지 않으면 이 함수는 FALSE를 리턴한다. 성공적으로 이벤트 커널 오브젝트가 생성되면 EnterCriticalSection은 잘동작하고 절대 예외를 발생하지 않는다.



※ EnterCriticalSection 실행 시 다른 스레드가 해당 임계영역을 소유하고있다면 무기한 대기를 하는것이 아니고 레지스트리에 등록된 시간만큼만 대기하고 하나의 예외를 발생한다.

위치는 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manger 에 위치한 CriticalSectionTimeout 값이다. 단위는 초이고 기본값으로 2592000초이다. 대략 30일이다.;; (한마디로 무한대기구먼.ㅡ_ㅡ;;)