🎯
스레드는 메모리의 구조 중 스택 영역을 제외한 코드/데이터/힙 영역을 공유한다. 이 때 여러 스레드가 동시에 같은 자원에 접근하는 것을 막기 위해 동기화 기법을 취해야 한다. 이 동기화는 유저 모드 동기화, 커널 모드 동기화로 나눌 수 있는데, 여기서 알아볼 세마포어와 뮤텍스가 바로 '커널 모드 동기화'의 일종이다. 우선 이 '공유'와 관련해 Critical Section이 무엇인지 짚은 뒤 세마포어와 뮤텍스에 대해 알아보자.
유저모드 동기화 / 커널모드 동기화
- 유저 모드 동기화 : 커널의 힘을 빌리지 않는(커널 코드가 실행되지 않는) 동기화 기법
- 커널 모드 동기화 : 커널에서 제공하는 동기화 기능을 활용하는 방법
임계 영역이란 프로그램에서 임계 자원을 이용하는 부분으로, 공유 자원의 독점을 보장하는 코드 영역을 의미한다.
쉽게 말해, 공유되는 자원, 즉 동시접근하려고 하는 자원이 있을 때 이를 다루는 부분에서 문제가 발생하지 않게 독점을 보장해줘야 하는 코드 영역을 의미한다.
임계구역은 시간이 지나면 종료되며, 어떤 프로세스가 임계구역에 접근하기 위해서는 지정된 시간만큼 대기해야 한다. 이때 쓰레드나 프로세스가 배타적인 사용권을 보장받기 위해서 세마포어 같은 동기화 메커니즘이 사용된다.
임계구역 문제를 해결하기 위한 3가지 조건
임계구역에서 발생하는 문제들은 3가지 조건을 충족시키면 해결할 수 있다.
Mutex 상호 배제(Mutual Exclusion)의 약자로 락(Lock)이라고도 한다. 이는 한 스레드, 프로세스에 의해 소유될 수 있는 Key를 기반으로 한 상호배제기법이다.
이는 임계구역(Critical Section)을 가진 쓰레드들의 실행시간(Running Time)이 서로 겹치지 않고 각각 단독으로 실행되도록 하는 기술인 **동시성 제어(Cuncurrency Control)를 시행하도록 설계되었으며, 실제 구현 방법은 다양하다.
이것은 프로그램이 시작될 때 고유한 이름으로 생성된다. 뮤텍스는 Locking 메커니즘으로 오직 하나의 쓰레드만이 동일한 시점에 뮤텍스를 얻어 임계 영역(Critical Section)에 들어올 수 있다. 그리고 오직 이 쓰레드만이 임계 영역에서 나갈 때 뮤텍스를 해제할 수 있다.
따라서 뮤텍스는 아래 두 가지 연산만을 지원하면 된다.
lock: 현재의 임계 구역에 들어갈 권한을 얻어온다. 만일 다른 프로세스/스레드가 임계 구역을 수행 중이라면 종료할때까지 대기한다(entry section).
unlock: 현재의 임계 구역을 모두 사용했음을 알린다. 대기중인 다른 프로세스/스레드가 임계 구역에 진입할 수 있다(exit section).
세마포어는 현재 공유자원에 접근할 수 있는 쓰레드, 프로세스의 수를 나타내는 값을 두어 상호배제를 달성하는 기법이다.
세마포어는 Signaling 메커니즘이라는 점에서 뮤텍스와 다르다. 세마포어는 락을 걸지 않은 쓰레드도 Signal을 보내 락을 해제할 수 있다는 점에서, wait 함수를 호출한 쓰레드만이 signal 함수를 호출할 수 있는 뮤텍스와 다르다.
즉, 뮤텍스는 Locking 메커니즘으로 락을 걸은 쓰레드만이 임계 영역을 나갈때 락을 해제할 수 있고, 세마포어는 Signaling 메커니즘으로 락을 걸지 않은 쓰레드도 signal을 사용해 락을 해제할 수 있다.
세마포어는 동기화를 위해 wait와 signal이라는 2개의 atomic operations를 사용한다.
wait를 호출하면 세마포어의 카운트를 1줄이고, 세마포어의 카운트가 0보다 작거나 같아질 경우에 락이 실행된다.
세마포어의 카운트가 0보다 작거나 같아져 동기화가 실행된 상황에, 다른 쓰레드가 signal 함수를 호출하면 세마포어의 카운트가 1증가하고, 해당 쓰레드는 락에서 나올 수 있다.
세마포어는 크게 Counting Semaphores, Binary Semaphore 2종류가 있다. 카운팅 세마포어는 세마포어의 카운트가 양의 정수값을 가지며, 설정한 값만큼 쓰레드를 허용하고 그 이상의 쓰레드가 자원에 접근하면 락이 실행된다. 바이너리 세마포어는 세마포어의 카운트가 1이며 Mutex처럼 사용될 수 있다.(뮤텍스는 절대로 세마포어처럼 사용될 수 없다.)
뮤텍스는 화장실이 하나밖에 없는 식당과 비슷하다. 화장실을 가기 위해서는 카운터에서 열쇠를 받아 가야 한다.
당신이 화장실을 가려고 하는데 카운터에 키가 있으면 화장실에 사람이 없다는 뜻이고 당신은 그 열쇠를 이용해 화장실에 들어갈 수 있다.
만약 당신이 화장실 안에 있을 때, 다른 사람이 화장실에 가고 싶어졌다. 이 사람은 아무리 용무가 급하더라도 열쇠가 없기 때문에 화장실에 들어갈 수 없다. 결국 남자는 당신이 용무를 마친 후 나올 때까지 카운터에서 기다려야 한다. 또 다른 사람이 와도 화장실에 들어가기 위해서는 카운터에서 대기해야 한다.
당신이 화장실에서 나와 카운터에 키를 돌려놓으면, 이제 기다리던 사람들 중 맨 앞에있던 사람은 키를 받을 수 있고 이를 이용해 화장실에 갈 수 있다.
이처럼, 뮤텍스는 Key 에 해당하는 어떤 오브젝트가 있으며 이 오브젝트를 소유한 (쓰레드,프로세스) 만이 공유자원에 접근할 수 있다.
세마포어는 손님이 화장실을 좀 더 쉽게 이용할 수 있는 레스토랑이다. 세마포어를 이용하는 레스토랑의 화장실에는 여러 개의 칸이 있다. 그리고 화장실 입구에는 현재 화장실의 빈 칸 개수를 보여주는 전광판이 있다.
만약 당신이 화장실에 가고 싶다면 입구에서 빈 칸의 개수를 확인하고 빈 칸이 1개 이상이라면 빈칸의 개수를 하나 뺀 다음에 화장실로 입장해야 한다. 그리고 나올 때 빈 칸의 개수를 하나 더해준다.
모든 칸에 사람이 들어갔을 경우 빈 칸의 개수는 0이 되며 이때 화장실에 들어가고자 하는 사람이 있다면 빈 칸의 개수가 1로 바뀔 때까지 기다려야 한다. 사람들은 나오면서 빈 칸의 개수를 1씩 더하고, 기다리던 사람은 이 숫자에서 다시 1을 뺀 다음 화장실로 들어간다.
세마포어도 이전과 똑같이 화장실이 공유자원이며 사람들이 쓰레드, 프로세스이다. 그리고 화장실 빈칸의 개수는 현재 공유자원에 접근할 수 있는 쓰레드,프로세스의 개수를 나타낸다.
두 기법 모두 완벽한 기법은 아니다. 이 기법들을 쓰더라도 데이터 무결성을 보장할 수 없으며 데드락이 발생할 수도 있다. 하지만 상호배제를 위한 기본적인 기법이며 여기에 좀 더 복잡한 매커니즘을 적용해 꽤나 우아하게 동작하는 프로그램을 짤 수 있다.
📝 정리하기
뮤텍스는 key를 기반으로 한 상호배제기법이고, 세마포어는 공유자원에 접근 가능한 스레드/프로세스의 수를 두는 상호배제기법이다.