본문 바로가기
네트워크, 서버/멀티쓰레딩, 비동기처리

(멀티쓰레딩, 비동기처리)다중 쓰레드 동기화 - Mutex

by 흥부와놀자 2020. 10. 13.

임계영역이란?

- 함수 내에 둘 이상의 쓰레드가 동시에 실행하면 문제를 일으키는 하나 이상의 문장으로 묶여있는 코드블록

 

이러한 임계영역에서 문제를 피하고 다중 쓰레드를 동기화 시키는데 여러 기법들이 존재한다. 운영체제 마다 각 기법의 구현방법이 다르다.

 

Mutex

임계영역을 통과할때 사용하는 자물쇠라고 보면된다.

- 과정

뮤텍스 오브젝트를 초기화 해준 후 임계영역 진입부에 뮤텍스락을 걸어준다.  현재 진입한 스레드 외에 다른 스레드는 락을건 부분에서 락이 풀릴때까지 블로킹 되며 진입스레드가 작업 마치고 락을 풀고 나가면 기다리던 스레드 중 하나가 다시 진입하고 락을 걸게된다.

모든 임계영역내 작업을 마친 뒤 해당 뮤텍스 오브젝트를 해제하면 끝난다.

 

하지만 이렇게 락을 걸었어도 락을건 Blocking상태에서 계속 머물게 되는 Dead-Lock에 빠질 위험이 존재한다. 

Dead-Lock관련해선 뒤에 다시 설명하도록 하겠다.

Mutex적용 전

위의 예시는 두개의 스레드가 하나의 변수에 접근했을때의 예이다. 원래대로라면 두스레드가 진입했을때 총 10만번의 계산 후 num이 10만이 되야 하지만 결과는 그렇지 않다.

 

그 이유는 num++하는 코드가 atomic하지 않아서 인데, num++은 두가지 과정을 거친다. 

 

num++ 어셈블리코드

어셈블리과정을 보면 num변수의 주소값을 프로세서내의 레지스터에 저장한다. 해당 레지스터에서 add연산을 수행한 후 다시 num변수의 주소값에 해당 레지스터의 내용을 넣는다. 

 

내부적으로 이러한 과정을 거치기 때문에 만약 한 쓰레드가 num의 주소값을 가져와서 내부 레지스터에서 연산하던 중 다른 쓰레드에서 값을 가져와 연산 후 저장한다면 해당 값을 무시하고 연산결과를 덮어 씌울것이다. 이렇기 때문에 공유자원에 대해 atomic한 연산이 아니라면 항상 하나의 쓰레드만 접근하도록 해야한다.

 

 

Mutext 적용 후

Mutex는 이렇게 해당 구역에 하나의 쓰레드만 들어갈 수 있게 한다는것을 알수 있다.

 

- OS에 따른 구현 차이

운영체제마다 구현방식의 차이가 있다. Posix계열에서는 pthread_mutex_lock / pthread_mutex_unlock함수로 락을 걸었다 풀면된다.

 

윈도우에서는 일단 유저모드에서 동작하는 CriticalSection과 커널모드에서 동작하는 Mutex로 나뉘어 진다.

CriticalSection은 커널모드로 trap하는 과정이 없기에 빠르지만 timeout등의 기능이 빠져서 DeadLock가능성이 있고 제약이 있다. EnterCriticalSection / LeaveCriticalSection 함수로 동작한다.

 

커널모드의 Mutex는 따로 Lock함수를 구현하지 않는대신 WaitforSingleObject로 락의 블로킹 기능을 대신한다. CreateMutex시 생성된 뮤텍스의 소유권을 해당 스레드가 가질지 , 다른스레드들한테도 열어놓을지 결정하는데 만약 다른 스레드들한테도 열어 놓으면 해당 뮤텍스(커널오브젝트)의 signaled 상태가 signaled로 된다. 그러면 처음 WaitforSingleObject에 진입하는 스레드가 해당 함수를 리턴하고 해당 뮤텍스는 auto-reset모드의 커널오브젝트이기 때문에 non-signaled상태로 바뀐다. 그러면 ReleaseMutex를 해서 singnaled로 바뀌기 전까지 다른 쓰레드들은 블로킹된다. 

 

Semaphore

세마포어는 기존 뮤텍스에서 발전하여 원하는 만큼의 스레드가 임계영역에 들어가도록 허용시킨 기법이다.

 

- 과정

처음 세마포어를 만들때 초기값을 지정하고 해당 값을 더하거나 빼는데, 만약 해당 값이 0이라면 블로킹에 들어간다. 즉 세마포어값은 해당 세마포어가 허용한 스레드의 개수라고 볼수있겠다. 세마포어의 허락을 받고 임계영역에 진입한 스레드는 영역을 나갈때 세마포어값을 올리는 함수를 호출하여 다른 스레드에게 기회를 준다. 

 

만약 세마포어값이 1일때는 바이너리 세마포어라고 하는데 이는 뮤텍스와 동일하게 작동한다.

 

초기값이 1인 세마포어

위의 예는 하나의 함수에 세개의 스레드가 붙었고 초기값이 1인 세마포어를 둠으로써 Mutex와 동일하게 작동하는 모습이다.

만약 초기값에 1보다 큰 수를 넣으면 어떻게 될까?

초기값이 2인 세마포어

초기값이 2이기 때문에 두개의 스레드가 같이 들어가게 된다. 그리고 하나의 스레드가 끝내고 세마포어의 값을 올리면 대기하던 다른 스레드가 들어간다.

 

- OS에 따른 구현차이

기본적으로 Mutex와 별반 다르지 않다.

Posix계열에선 sem_post로 값을 올리고 sem_wait으로 값을 내리며 0이면 블로킹시킨다.

 

윈도우에서는 만약 세마포어값이 0이면 해당 세마포어 커널오브젝트를 non-signaled로 바꾸고 0보다 크면 singnaled로 바꾼다. 그렇기에 WaitforSingleObject를 사용하여 세마포어값을 1감소시키고 0보다 작으면 블러킹 상태를 만든다. 나갈때는 ReleaseSemaphore함수를 통해 원하는 값만큼 값을 증가시킬수 있다.