Critical Section
Updated:
동기화
시스템의 자원은 한정적인데 이 한정적인 자원에 여러 스레드가 동시에 접근해서 사용하려하면 문제가 발생할 수도 있습니다. 이런 문제를 방지하기 위해 스레드들에게 하나의 자원에 대한 처리 권한을 주거나 순서를 조정해주는 기법입니다.
공유된 자원 속 하나의 데이터는 한번에 하나의 프로세스만 접근할 수 있도록 제한해 두어야 할 필요성이 있는데 이를 위해 고안된 것이 Semaphore(세마포어)입니다.
유명한 화장실 예제로 쉽게 설명해보겠습니다.
공중 화장실은 한번에 1명만 사용할 수 있다고 가정하겠습니다. 어떤 사람이 사용하고 있는데 다른 누군가가 갑자기 들어와서 사용할 수가 없습니다. 또한, 화장실 안에 들어올 수 도 없고요.
이를 위해 화장실 열쇠를 만들어서 이용할 수 있습니다. 열쇠를 가지고 있는 한 사람만 화장실을 이용하고, 열쇠가 없는 사람은 밖에서 대기를 하죠. 여기서 열쇠는 세마포어와 같은 역할을 한다고 할 수 있습니다.
임계영역(Critical Section)
둘 이상의 프로세스에 의해서 동시에 접근하면 안되는 공유자원에 접근하는 코드 영역이라고 한다. 다수의 프로세스가 접근 가능한 영역이면서 한 순간에 반드시 하나의 프로세스만 사용할 수 있는 영역입니다.
위 그림에 Memory 구조에서 공유 메모리는 전역변수 Money가 있는 0x01F0 이며, 코드 영역은 5 ~ 7, 12 ~ 14번째 line입니다.
Semapore
프로세스간의 시그널(신호, Signal)을 주고받기 위해 사용되는 정수 값, 리소스의 상태를 나타내는 카운터로 세마포어는 다음 세가지 원자적인 연산만을 지원합니다.
initialize, decrement, increment
- initialize : 세마포어 초기화. (음이 아닌 정수값으로 초기화)
- decrement : 프로세스를 블록시킬 수 있습니다.
- increment : 블록되었던 프로세스를 깨울 수 있습니다. 이 세마포어를 카운팅 세마포어 또는 범용 세마포어라고 합니다.
세마포어의 값에 따라 운영체제는 프로세스가 즉시 자원을 사용할 지, 자원이 다른 프로세스에 의해 사용 중인걸 알게 될 경우엔 일정 시간을 기다려야 합니다.
프로세스가 자원을 사용하는 동안에는 세마포어 값을 변경함으로서 다른 프로세스들이 기다리게 해야합니다.
- 프로세스간 메시지를 전송하거나 공유메모리를 통해 특정 데이터를 공유하게 될 경우 공유 자원에 여러 프로세스가 접근하면서 문제가 발생하게 됩니다.
하나의 프로세스만 자원에 접근 가능하도록 설정할 때 세마포어를 사용합니다.
위 화장실 예제로 다시 살펴보면, 세마포어는 1개 이상의 열쇠라고 할 수 있습니다. 만약 화장실 칸이 4개이고 열쇠가 4개라면, 4명까지는 대기없이 바로 사용할 수 있고 그 다음 부터는 대기를 해야하죠. 이것이 바로 세마포어입니다.
그러므로 몇개의 세마포어로 구성해서 운영체제의 리소스를 경쟁적으로 사용할지는 꽤 중요한 이슈입니다.
그림으로 표현하면 아래와 같습니다.
새머포어 연산
세마포어는 다익스트라가 제안한 프로세스 동기화를 위한 구조로, 이는 여러 프로세스들에 의해 공유되는 변수로 정의된다. 그런데 이 변수는 보통의 방법으로는 액세스할 수 없고 오직 P와 V라는 연산으로만 다룰 수 있다. P와 V연산의 정의는 다음과 같다.
procedure P(S) --> 최초 S값은 1임
while S=0 do wait --> S가 0면 1이 될때까지 기다려야 함
S := S-1 --> S를 0로 만들어 다른 프로세스가 들어 오지 못하도록 함
end P
procedure V(S) --> 현재상태는 S가 0임
S := S+1 --> S를 1로 원위치시켜 해제하는 과정. 이제는 다른 프로세스가 end V 들어 올수 있음
P와 V는 쪼갤수 없는 단일 연산이다. 즉 한 프로세스가 P나 V를 수행하고 있는 동안에는 프로세스가 인터럽트를 당하지 않는다. 이제 P와 V를 사용하면 다음과 같이 위험지역(cirtical section)에 대한 상호배제를 구현할수 있다.
P(S);
--------------------
| Critical Section |
--------------------
V(S);
최초에 S의 값은 1이다. 위와 같은 위험지역을 포함하는 두개의 프로세스 A와 B가 있다고 하자. A와 B는 서로 독립적으로 수행되지만, 두 프로세스가 동시에 위험 지역으로 들어가서는 안된다.
위와 같이 세마포어를 사용하면 P(S)를 먼저 수행하는 프로세스가 S를 0으로 해놓고 위험지역에 들어가므로 나중에 도착하는 프로세스는 P에서 더이상 진행되지 못하고 기다리게 된다.
먼저 들어갔던 프로세스가 V(S)를 해주어야 비로서 P(S)에서 기다리던 프로세스가 위험지역에 들어갈 수 있고 따라서 상호배제가 실현된다.
Mutex
MUTual EXclusion으로 상호배제라고도 합니다.
0또는 1의 값을 가지는 이진 세마포어와 유사합니다. Critical Section (임계구역)을 가진 스레드들의 실행 시간을 서로 겹치지 않게 단독으로 실행하게 하는 기술입니다. 즉, 공유된 자원의 데이터를 여러 쓰레드가 접근하는 것을 막는 것이라고 보면 됩니다.
- 프로세스들의 공유 리소스에 대한 접근을 조율하기 위해 Locking과 Unlocking을 사용합니다.
- 뮤텍스 객체를 두 스레드가 동시에 사용할 수 없습니다.
즉, Critical Section을 가진 쓰레드들의 Running tme이 서로 겹치지 않게 각각 단독으로 실행되게 하는 기술입니다. 다중 프로세스들이 공유 리소스에 대한 접근을 조율하기 위해 locking과 unlocking을 사용합니다.
간단히 말해, Mutex객체를 두 쓰레드가 동시에 사용할 수 없다는 말입니다.
위 화장실 예제로 다시 살펴보면, 뮤텍스는 무조건 1개의 열쇠만 가질 수 있습니다.
그림으로 표현하면 아래와 같습니다.
Semaphore vs Mutex
- 세마포어는 뮤텍스가 될 수 있지만, 뮤텍스는 세마포어가 될 수 없습니다.
- 세마포어는 소유할 수 없으며, 뮤텍스는 소유할 수 있고 소유주가 그에 대한 책임을 집니다.
- 뮤텍스의 경우 뮤텍스를 소유하고있는 스레드가 이 뮤텍스를 해제할 수 있습니다. 하지만, 세마포어는 소유하지 않고 있는 스레드가 세마포어를 해제할 수 있습니다.
- 세마포어는 시스템 범위에 걸쳐있고 파일 시스템 상의 파일 형태로 존재합니다. 하지만, 뮤텍스는 프로세스 범위를 가지고 프로그램이 종료될 때 자동으로 지워집니다.
- 세마포어는 동기화 대상이 여러개 일 때, 뮤텍스는 동기화 대상이 오로지 하나 일 때 사용됩니다.
Monitor
동기화 문제를 해결하기 위해서 우리는 Semaphore와 Mutex라는 도구를 사용하였다. 하지만 동기화 문제를 해결하는데 세마포만이 사용되지는 않는다. 사실 세마포의 경우 오래된 동기화 도구라고 할 수 있다.
현재 사용되는 도구 중 하나가 모니터이다. 특히 자바 프로그램에서는 모니터에 대한 활용이 높다. 세마포가 어셈블리 언어에 적합한 도구라면 모니터는 그보다 고수준인 언어의 도구라고 할 수 있다.
공유자원과 공유자원에 대한 접근함수가 존재한다. 이러한 구역을 임계구역이라고 한다. 모니터의 경우 두 개의 queue가 있는데 각각 배타동기와 조건동기의 역할을 한다. 배타동기의 queue는 하나의 쓰레드만 공유자원에 접근할 수 있게 하는 작용을 하는 공간이다.
특정 쓰레드가 공유자원을 사용하는 함수를 사용하고 있으면 다른 쓰레드는 접근을 할 수 없고 대기해야 한다. 조건 동기의 queue는 진입 쓰레드가 블록되면서 새 쓰레드가 진입가능하게 하는 공간이다. 새 쓰레드는 조건동기로 블록된 쓰레드를 깨울 수 있다. 깨워진 쓰레드는 현재 쓰레드가 나가면 재진입할 수 있다.
자바의 모든 객체는 모니터가 될 수 있다. 배타 동기는 synchronized 키워드를 사용해서 지정할 수 있고 조건 동기는 wait()함수와 notify()함수, notifyAll()함수를 사용한다. 배타 동기를 지정하는 함수들은 공통 자원을 사용하고 있는 경우이다. 공통 자원을 사용할 경우 배타 동기를 선언하는 synchronized라는 키워드를 적어주기만 하면 상호배타의 원리를 만족시키는 함수로 만들어준다. 조건 동기의 경우 wait()함수를 실행하면 진입한 쓰레드를 조건 동기 queue에 블록을 시킨다. notify()함수는 그렇게 블록된 함수를 깨우는데 새로운 쓰레드가 실행하는 방식으로 깨우게 된다. notifyAll()은 모든 쓰레드를 깨우는 것으로 사용할 수 있다.
세마포의 경우와 비교를 해볼 수 있다. 세마포의 경우 임계구역 앞에 설치되어 초기 값을 설정해 들어갈 수 있는 한계를 놓는다. 들어갈 때 acquire()명령하고 나올 때는 release()명령을 실행시켜 주어야한다. 하지만 이런 관계를 기억하는 것이 힘들다. 이와 반대로 모니터는 따로 명령을 불러줄 필요 없이 함수에 synchronized만 붙여 넣으면 상호배타의 기능을 수행할 수 있다.
Leave a comment