티스토리 뷰

뮤텍스와 세마포어에 대한 비교글이 많은데, 딱히 와닿지 않아서 3 step os를 보고 직접 정리해보았다.

조금 디테일한 내용은 있으나, 전반적인 사전지식을 위해 추가한것이므로 그렇게 깊게 다루진 않았다.

 

1. Mutex 

뮤텍스는 기본적으로 상호배제(mutural exclusion) 를 위한 lock을 위해 고안된 아이디어이다.

POISX 표준에 등록되어있어서 mutex라는 이름으로 널리 쓰이는것 같다.

https://www.joinc.co.kr/w/man/3/pthread_mutex_init

 

linux man page : pthread_mutex_init - mutex 를 초기화 한다.

뮤텍스(mutex)는 쓰레드가 공유하는 데이터 영역을 보호하기 위해서 사용되는 도구이다. pthread_mutex_init는 뮤텍스 객체를 초기화 시키기 위해서 사용한다. 첫번째 인자인 mutex는 초기화 시킬 mutex객

www.joinc.co.kr

예를들어서, balance의 값을 thread-safe하게 바꾸고 싶다면 아래와 같이 사용할 수 있다.

int balance = 1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
Pthread_mutex_lock(&lock); // wrapper; exits on failure
balance = balance + 1;
Pthread_mutex_unlock(&lock);

 

락을 구현/해제에서 등에서 사용되는 구현 솔루션은 굉장히 여러가지가 있지만 이 글의 메인 토픽은 아니므로 넘어가겠다. 

뮤텍스를 한마디로 정리하자면, 바로 그냥 단순히 상호배제를 위해 lock을 거는 솔루션들을 의미한다고 볼 수 있다.

 

3 step os에서도 뮤텍스 자체에 대한 의미보다는, 어떻게 mutex를 구현할건지에 대해서 많은 부분을 할애한다.

 

2. Semaphore

세마포어는 다익스트라가 고안한 (그 "최단거리 알고리즘"을 고안한) 동시성 컨트롤 방식이다.

이 방식이 특이한 점은 바로 init value라는게 존재하는데, init value를 다양하게 조정하므로써 다양하게 사용을 연출할 수 있다.

 

https://man7.org/linux/man-pages/man0/semaphore.h.0p.html

 

semaphore.h(0p) - Linux manual page

semaphore.h(0p) — Linux manual page semaphore.h(0P) POSIX Programmer's Manual semaphore.h(0P) PROLOG         top This manual page is part of the POSIX Programmer's Manual. The Linux implementation of this interface may differ (consult the correspondi

man7.org

표준적인 함수 중에서 가장 중요한 세가지 main 함수들만 살펴보도록 하겠다.

- int sem_init(sem_t *sem, int pshared, unsigned value);

- int sem_wait(sem_t *);

- int sem_post(sem_t *);

 

sem_init은 1. semaphore 타입의 변수와,2. 타입, 그리고 3. 초기값을 배정한다.

sem_init의 값은 한번 설정된 이후에는 읽거나 수정할 수 없다. (당연하겠지만..)

 

1. 세마포어 타입의 변수는 그냥 sem_t 타입의 변수 (글로벌 변수같은) 것을 넣어주면 되는거라 복잡하지 않다.

2. 두번째 argument는 같은 프로세스 내에서 공유되는지, 쓰레드 사이에서 공유되는지를 체크하는지 보면 된다.

만약 0이 오면, 같은 프로세스 내의 쓰레드 사이에서 동시성을 컨트롤 한다고 인지하면 되고, 0이 아닌 값이 오면 프로세스 들 사이에서 컨트롤 한다고 생각하면 된다. 앞으로 나오는 모든 예제에서는 같은 프로세스 내의 쓰레드에서 공유된다고 가정한다.

 

쓰레드 뿐만 아니라 프로세스까지 컨트롤 하는 부분은 세마포어가 가지고 있는 특징이라고도 할 수 있지만, 우리가 다룰 부분에서 핵심적으로 다룰 부분은 아니므로 넘어간다.

 

3. 세번째 argument는 바로 세마포어의 초기값이 들어오는 부분이다.

 

바로 이 세번째 argument의 값에 따라서 세마포어를 다양한 동시성 상황에서 사용할 수 있다.

어떻게 할 수 있는지는 sem_wait과 sem_post까지 보고 자세하게 알아본다.

 

 

[sem_wait]은 sem 값을 1을 낮춘다. 만약 sem값이 음수값이라면, 리턴하지 않고 wait시킨다.

예를들어서 sem_init의 초기값이 0이라고 해보고, 그 이후에 sem_wait을 부르면

0 -> -1로 바꾼 후에, -1은 음수값이므로 wait에 들어간다.

만약 다른 쓰레드도 sem_wait()을 부르게되면 sem값은 -2가 된다.

 

눈치를 챈 사람도 있겠지만, sem값의 수는 결국에는 현재 기다리고 있는 쓰레드의 숫자를 나타낸다.

 

[sem_post] 는 반대로 1을 높인다. 그리고 대기를 하고 있던 쓰레드를 실행시킨다.

 

1. 세마포어를 Mutex에서 활용하기.

초기값의 특징을 잘 활용하면 이 세마포어를 mutex처럼 사용할 수 있다.

 

만약 세마포어의 init값을 1로 설정하게되면,

(1) 쓰레드 A가 sem_wait()을 건다. // sem값 0.

(2) 쓰레드 B가 sem_wait()을 건다.  // sem값 -1 -> return하지 못하고 sleep 시킨다.

(3) 쓰레드 A가 할일을 다 끝내서 sem_post()를 부른다 // sem값 0.

(4) 자고있던 쓰레드 B가 깨어나서 sem_wait()을 리턴한다. 그리고 그 부분을 수행한다.

(5) 끝났으면 sem_post()를 수행한다. // sem값 1.

 

초기값이 1이기 때문에 가능한 시나리오였다.

조금 더 자세한 시나리오는 아래에서 확인할 수 있다.

https://pages.cs.wisc.edu/~remzi/OSTEP/threads-sema.pdf

 

2. 세마포어를 Ordering에서 사용하기.

세마포어를 쓰레드별로 순서를 강제할 때 사용할 수 있다.

이를 이해하기 위해서는 일단 Condition variables에 대해서 알아야 한다.

간단하게 설명하자면 thread의 join()같은 역할을 하기 위해서 사용하는 변수들이라고 보면 된다.

 

사전지식을 위한 topic : Condition Variables

condition은 바로 멀티 쓰레드 환경에서 정말 이 시점에 이걸 실행해도 되는지에 대한 조건을 의미한다.

대표적으로 아래의 사진에서의 코드를 보자.

https://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf

11번째 라인에서, 자식에 대한 쓰레드를 만들고, 12번째 라인은 반드시 11번째 라인이 끝난 뒤에 호출하고 싶다. 

그럼 어떻게 기다리게 할까? Mutex같은 lock방식에서의 상황과는 또다른 문제라고 볼 수 있다. Mutex는 단순히 상호배제를 위해 막기 위한것 이라면, 이것은 쓰레드간 순차 실행을 위해 "기다림 (sleep, wait)" 이라는 조건이다.

 

가장 간단하게는 while()같은것에 done이라는 변수를 둬서 while(done ==1 ) 이 끝날 때 까지 계속 spin 시키는 방법이다.

간단한 방법이긴 한데 spin loop은  기본적으로 CPU를 쓸데없이 점거하는 방법 중 하나이다. (CPU가 할당받은 시간동안 쓸데없이 while문만 돌고 끝나니 좋은 방법일리 만무하다)

 

done같은 conditiontrue로 변하게 만들기 위해서 기다리기 위해 Condition variables 라는것을 사용할 수 있다. 다시말해 저런 done같은 상태를 관리해주는 큐같은 자료구조 정도로 생각해두면 좋을것 같다. 예를들어, parent condition_wait 상태에 들어가면 자체적으로 condition variables queue + sleep 에 들어가서 waiting을 지속할 수 있고, child가 condition variables에 signal을 보내서 parent가 깨어나게 할 수 있다.

 

이도 역시 POISX의 표준으로 등록되어 있으며, 이를 통해서 쉽게 관리할 수 있다.

https://linux.die.net/man/3/pthread_cond_init

 

pthread_cond_init(3) - Linux man page

pthread_cond_init(3) - Linux man page Prolog This manual page is part of the POSIX Programmer's Manual. The Linux implementation of this interface may differ (consult the corresponding Linux manual page for details of Linux behavior), or the interface may

linux.die.net

이를 통해서 위의 상황을 개선해보겠다.

 

위의 사진에서 11번째 줄에 들어갈 부분을 join()으로 대체하고, 

pthread_cond_wait()을 사용하여, condition variable을 사용해준다. 

 

한가지 특이한점은 pthread_cond_wait()의 인자값들을 보면 lock변수인 &m까지 같이 가지고 들어가는점인데,

pthread_cond_wait()을 부를 때는 이미 lock이 걸려있는 상태라고 가정하고, sleep->lock해제까지 하고 잠에 든다.

lock을 가지고 있고 sleep에 들어가면 영원히 lock을 못풀어 주는 상태에 걸릴 수 있기 때문이다. (sleep 들어가기 전에 lock푸는건 국룰이다.)

 

 

위의 복잡하기도 한 과정을 요약하면 아래와 같다.

x,y가 실행되고 parent는 cond_wait을 만나 본인의 쓰레드를 condition variables에 넣어준다.

child는 모든 일이 다 끝나면, signal을 보내 parent를 모두 종료까지 시킨다.

 

물론 위의 구현이 완벽한 구현은 아니다. 하지만 어떤 느낌인지 설명하기 위해 에시를 가져왔다.

 

다시 세마포어로 돌아와서,

세마포어도 저런 condition variables같은 역할을 할 수 있다.

세마포어를 다양하게 사용하기위해서는 init값을 잘 활용해야 한다고 했는데, 여기서는 init값을 어떻게 활용할 수 있을까?

바로 0으로 지정하면 CVs를 대체하여 활용가능하다.

 

parent에서 sem_wait에 들어가게 되면 바로 sleep상태에 들어가게 된다. 왜냐하면 0에서 1을 빼면 -1이고, -1이라면 sleep이 되기 때문이다.

child에서 작업이 다 끝나면 sem_post를 통해서 parent를 깨울 수 있다.

 

조금 더 심화 내용.

프로듀서 컨슈머 

프로듀서와 컨슈머가 존재하고, 프로듀서와 컨슈머 사이에 최대 1개의 엘리먼트를 담을 수 있는 버퍼가 있다고 해보자.

프로듀서 컨슈머 문제를 잘 모르는 사람들이라면, 각 쓰레드에서 하나의 공유 자원들을 가져오는 경우 

1. 프로듀서는 꽉 차 있는 버퍼에 더이상 넣으면 안될것이고 (빌때까지 대기해야한다)

2. 컨슈머는 비어있는 버퍼에서 가져오면 안된다. (대기해야한다)

두가지 문제를 어떻게 현명하게 대처할것인지 생각해볼 수 있다.

 

프로듀서와 컨슈머 사이에 동시성을 유지하면서 값을 가질 수 있도록 하려면 어떻게 해야할까? 세마포어를 활용해보자.

1. 세마포어를 두개 쓴다. empty & full .

empty는 버퍼사이즈인 1로

full은 0으로 해준다.

 

앞서 봤던 지식으로 보면, empty는 binary lock을 활용하는 용도로

full은 condition variables의 용도로 사용하는것으로 추측할 수 있다.

 

2. 프로듀서는 처음에 empty에 wait()을 걸어놓고 시작한다. 그럼 empty가 0으로 변한다. 만약 한번더 producer가 채우려는 시도를 하면? sleep상태에 들어간다. 이 상황에서 버퍼의 사이즈는 1이기 때문에 더 넣으면 sleep으로 들어가는게 맞다!

 

3. 프로듀서는 채우는 작업이 끝났으면 post()를 full에 날려준다. 이로써, 버퍼가 가져갈 수 있는 용량이 1개 있다는 상태를 알릴 수 있다.

 

4. 컨슈머는 처음에 full에 wait()을 체크한다. full 버퍼의 sem값은 바로 버퍼가 비어있는지 체크할 수 있기 때문에 만약 0이라면 -1로 바꾸고 wait상태로 들어갈 수 있다. 그리고 use()를 통해 버퍼에 있는 값을 가져온다. 그리고 emptyBuffer를 통해서, 버퍼가 현재 비어있다는 사실을 알려줄 수 있다.

 

처음에 "앞서 봤던 지식으로 보면, empty는 binary semaphore lock을 활용하는 용도로

full은 condition variables의 용도로 사용하는것으로 추측할 수 있다."

이런말을 했었는데, 실제로 작동하는 방식을 보니까 empty, full의 sem 정수값이 특정 상태를 알려주는것을 알 수 있다.

 

empty의 sem값은 현재 버퍼에 가용할 수 있는 값을 + 나타낸다.

full의 sem값은 현재 버퍼에 채워져 있는 값이 몇개인지 -로 나타낸다.

 

이런 의미에서 다시보게되면, 훨씬 더 이해가 쉽다.

프로듀서는 현재 버퍼에 가용할 수 있는 값이 0이상인지 체크하고,

컨슈머는 현재 버퍼에 있는 값이 0이상인지 체크한다.

 

그럼 empty의 초기값은 항상 가용할 수 있는 버퍼의 크기여야 한다는것을 알 수 있다.

full의 초기값은 상관없이 0으로 나타냄을 알 수 있다.

 

이렇게 세마포어와 뮤텍스의 차이에 대해서 알아보았다.

 

짧게 정리하자면,

뮤텍스는 단순  lock변수 (key)를 기반으로한 상호 배제 기능을 제공하는 api.

한 쓰레드가 사용중에는 그 키를 사용하고, 돌려주지 않는다.

 

세마포어는 Singaling 메커니즘을 활용하여 lock부터 conditional variables의 기능까지 작동시키는 api. 

시그널링 매커니즘 자체가 키 매커니즘과 차이가 있긴 하다. 시그널링 매커니즘과 상태를 나타내는 init sem값을 통해서 다양한 동시성 상황에 대해서 대비 가능하다.

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

[C언어] 배열에 있는 값 옮기기  (0) 2019.02.13
[운영체제] 페이징 테이블 계산 기초  (5) 2019.02.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함