공부/운영체제

Windows에서의 동기화 기법

sudo 2022. 1. 22. 09:11

DirectX 공부를 하면서 게임을 만들고 있는데 멀티 쓰레드를 이용해서 로딩을 하기 위해 그에 대한 공부가 필요해져서 해보려고 한다. 사실 우리는 visual studio에서 이미 멀티 쓰레드 환경에서 프로그래밍을 하고 있다. 

MainThread가 내가 만든 프로그램을 실행하고 있고 다른 Thread들이 다른 dll을 실행중인걸 볼 수 있다

 

Window환경에서의 쓰레드 생성을 위해 필요한 함수

// 새로운 쓰레드를 생성한다
// 1번째 인자는 자식 프로세스에 상속할때 설정해주는 구조체의 포인터
// 2번째 인자는 stack size. 0을 넘겨주면 자동으로 설정된다
// 3번째 인자는 생성한 쓰레드가 동작하게 될 함수
// 4번째인자는 생성한 쓰레드가 동작하게 될 함수에 넘겨줄 인자
// 마지막 인자에는 ThreadID를 받고 싶다면 unsigned int* 타입의 변수를 넣어두면 된다
uintptr_t _beginthreadex( // NATIVE CODE
   void *security,
   unsigned stack_size,
   unsigned ( __stdcall *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr
);
// 1번째 인자는 Signaled 상태가 될때까지 Blocking상태로 관찰 대상 이벤트 오브젝트나
// 종료한지 안한지 관찰할 쓰레드의 handle을 넣어주면 된다.
// 2번째 인자는 Blocking상태로 대기하게 될 시간이다. INFINITE는 handle이 이벤트 오브젝트면 
// 이벤트 오브젝트가 signaled 상태가 될 때까지, 쓰레드면 쓰레드가 동작하는 함수가 종료되거나
// _endthreadex를 호출할 때 까지 무한정 Blocking 상태로 대기한다  
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);

 

동기화(Synchronization)

공유 자원이 존재할 때 프로세스나 쓰레드의 실행 시점을 조절해서 모든 프로세스나 쓰레드가 알고 있는 정보가 일치하게 만드는 것

 

이런 동기화를 위해서 필요한 것이 락(Lock)이라는 동작이다. 락이란

공유 자원을 하나의 쓰레드가 사용하고 있을 때 다른 쓰레드가 공유 자원을 사용하지 못 하도록 제한을 거는 것이다.

 

커널 모드에서 사용하는(= 커널 객체 사용하는) 동기화하는 방법도 있으며 유저 모드에서 동기화(= 커널 객체를 사용하지 않는)하는 방법도 있다. 기본적으로 커널 모드는 관리자 모드처럼 유저 모드의 권한으로 하지 못하는 여러 기능들을 할 수 있지만 유저 모드에서 커널 모드로의 전환이 필요하기 때문에 성능은 유저 모드가 더 좋다.

 

커널 객체를 사용한 동기화

- Spin Lock, Mutex, Semaphore, Event

 

Spin Lock, Mutex, Semaphore는 다른 포스팅에 따로 작성해놓았다(https://welikecse.tistory.com/121). 프로젝트에서는 우선 Event를 사용하고 있으니 먼저 살펴보자

 

Event

- 공유되는 Resource에 대한 동기화를 위해 사용되는 Mutex, Semaphore와 다르게 쓰레드의 작업 순서를 지정하기 위해 사용되는 객체. 코드를 실행하는 주체는 쓰레드지만 그 쓰레드의 상태(멈춰서 대기할지 실행할지)를 제어해주는 객체. Event객체를 얻어야 쓰레드는 실행할 수 있다. 이벤트 객체의 상태에 따라 쓰레드가 이벤트 객체를 얻을 수 있을수도 있고 없을수도 있는데

 

Event가 Signal상태 -> 쓰레드가 이벤트 객체를 얻어서 실행할 수 있음

Event가 Non-Signal 상태 -> 쓰레드가 이벤트 객체를 얻을 수 없어서 대기해야함

 

먼저 이벤트 객체는 CreateEvent로 만들어줄 수 있다

// 1번째 인자는 자식 프로세스에게 상속여부
// 2번째 인자는 False로 하면 자동 reset mode.
// 여기서 reset은 이벤트 객체가 signaled->non-signaled로 되는걸 의미한다.
// WaitForSingleObject를 빠져나와서 signaled 상태이면 수동 reset mode이고, non-signaled 상태이면 자동 reset mode이다
// 3번째 인자는 False로 하면 초기 상태를 non-signaled 상태로 생성
// 4번째 인자는 이벤트 이름
HANDLE StartEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

Event 객체도 쓰레드와 마찬가지로 WaitForSingleObject의 첫번째 인자로 Event의 HANDLE값을 넣어주면 WaitForSingleObject함수를 실행하는 쓰레드가 Event객체를 얻을 수 있을 때 까지 Blocking 상태로 만들 수 있다.

 

쓰레드가 Blocking에서 빠져나오게 하려면 SetEvent함수를 사용해서 Event 객체를 Non-Signal -> Signal 상태로 바꿔줘서 이벤트 객체를 얻을 수 있게 해야 한다.

//  Event 객체를 Non-Signaled -> Signaled 상태로 바꿔준다
BOOL SetEvent(
  [in] HANDLE hEvent
);

 

유저 모드에서의 동기화

- Critical Section

하나의 쓰레드가 Critical Section을 실행중일때, 다른 쓰레드가 Critical Section을 만나게 되면 원래 실행중이던 쓰레드가 Critical Section으로 지정된 영역의 실행을 마치고 떠날때 까지 대기하게 하는 동기화 방식이다. 결과적으로 Critical Section으로 지정된 코드 블럭은 한번에 하나의 쓰레드만 실행하게 된다. 

 

1. Critical Section

Critical Section 사용하기 위해서 우선적으로 InitializeCriticalSection으로 초기화를 해줘야한다. 인자로는 CRITICAL_SECTION 객체의 포인터가 들어가야 한다.

void InitializeCriticalSection(
  [out] LPCRITICAL_SECTION lpCriticalSection
);

 

초기화를 했다면 CRITICAL_SECTION을 만들어줄 수 있는데 방법은 CRITICAL_SECTION으로 지정하고 싶은 블록의 시작부분에 EnterCriticalSection, 끝부분에 LeaveCriticalSection를 써주면 된다

void EnterCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

void LeaveCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

예를 들어 Circular Queue를 구현했고 Circular Queue의 empty여부를 확인하거나 push/pop하는 함수를 동기화 시키고 싶다면 다음과 같이 하면 된다. 중요한건 A쓰레드가 어떤 하나의 CRITICAL_SECTION 객체와 Enter/LeaveCriticalSection 함수로 어떤 함수내부에 Critical Section을 형성해놓았고, A 쓰레드가 실행 도중에 B 쓰레드로 Switching됐을때, 똑같은 CRITICAL_SECTION 객체를 사용해서 Critical Section을 형성하려고 하면 B 쓰레드는 Blocking상태가 된다. 그게 A가 아닌 다른 쓰레드던, A 쓰레드가 실행하던 Critical Section이 다른 영역이다 하더라도.

CRITICAL_SECTION Crt;

void push(const T& data)
{   
  EnterCriticalSection(&Crt);	// 여기서부터 CRITICAL_SECTION 시작

  int	Tail = (m_Tail + 1) % m_Capacity;

  if (Tail == m_Head)
    return;

  m_Queue[Tail] = data;

  m_Tail = Tail;

  ++m_Size;
    
  LeaveCriticalSection(&Crt);	// 여기까지 CRITICAL_SECTION
}

void clear()
{
    EnterCriticalSection(&Crt); // 여기서부터 CRITICAL_SECTION 시작

    m_Head = 0;
    m_Tail = 0;
    m_Size = 0;
    
    LeaveCriticalSection(&Crt);	// 여기까지 CRITICAL_SECTION
}

예를 들어, 위의 두 함수는 전역 변수로 선언된 같은 CRITICAL_SECTION 객체를 사용중인데, 여기서 A 쓰레드가 push함수 실행 중간에 context switching이 일어났고, B 쓰레드가 clear를 호출해도 EnterCriticalSection에 Blocking된채로 대기하고 A 쓰레드가 다시 점유돼서 Critical Section을 빠져 나와서 B 쓰레드에 점유를 내주면 그제서야 Blocking에서 빠져나와 실행할 수 있다. 

 

그리고 더 이상 CRITICAL_SECTION을 사용하지 않을땐 반드시 DeleteCriticalSection으로 해제해줘야 한다.

void DeleteCriticalSection(
  [in, out] LPCRITICAL_SECTION lpCriticalSection
);

 

Reference

https://3dmpengines.tistory.com/612

 

동기화 함수들... [쓰레드 동기화(실행순서 동기화) -이벤트,타이머]

[네트워크C++ - 동기화]쓰레드 동기화(실행순서 동기화) -이벤트,타이머  쓰레드 / [네트워크기초] 2008/01/17 21:54 http://blog.naver.com/blue7red/100046389559 1.순서동기화의 이유 -생산자/소비자 모델..

3dmpengines.tistory.com

 

'공부 > 운영체제' 카테고리의 다른 글

시스템 콜(System Call)  (0) 2022.09.06
데드락(Deadlock)  (0) 2022.09.02
Spin Lock, Semaphore, Mutex  (0) 2022.09.02
쓰레드(Thread), 프로세스(Process)란?  (2) 2022.09.01