동기화란
병렬적으로 실행되는 프로세스들은 공동의 목적을 올바르게 수행하기 위해 협력해야한다. 이때 실행 순서와 자원의 일관성을 보장해야 하기 때문에 동기화되어야 한다.
동기화의 의미
프로세스의 동기화란 국어 사전에 따르면 ‘정보·통신 분야에서의 동기화란 작업들 사이의 수행 시기를 맞추는 것’이라는 의미를 갖는다. 정리히자면, 프로세스 동기화란 프로세스들 사이의 수행 시기를 맞추는 것을 의미한다.
프로세스뿐 아니라 스레드 같은 실행 흐름을 갖는 모든 것은 동기화의 대상이다. 다만 여기에서는 대부분의 전공서 표현에 따라 ‘프로세스 동기화’라고 칭한다.
프로세스들 사이의 수행 시기를 맞추는 것이 의미하는 것은 다음과 같다.
- 실행 순서 제어 : 프로세스를 올바른 순서대로 실행하기
- Reader와 Writer가 있을 때 Writer가 먼저 수행되고 나서 Reader가 수행되어야한다. 이때 두 프로세스가 순서대로 동작하도록 하는 것이 프로세스 동기화이다.
- 상호 배제 : 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하기
- 10만원이 있는 통장에서 2만원을 더하려는 프로세스와 5만원을 더하려는 프로세스가 있다. 금액을 더하는 과정은 값을 읽고, 더하고, 저장하는 순서로 있다.
만약 10만원 잔액에서 2만원을 더하기 위해 값을 읽고, 더했을때 문맥 교환이 일어나서 5만원을 더하는 프로세스가 잔액을 읽고 더하고 저장하여 잔액이 15만원이 되었다. 이때 2만원을 더하는 프로세스가 마저 진행되면 잔액이 12만원이 된다. 이렇게 동시에 접근해서는 안 되는 자원에 동시에 접근하지 못하게 하는 것이 상호 배제를 위한 동기화이다.
- 10만원이 있는 통장에서 2만원을 더하려는 프로세스와 5만원을 더하려는 프로세스가 있다. 금액을 더하는 과정은 값을 읽고, 더하고, 저장하는 순서로 있다.
생산자와 소비자 문제
상호 배제를 위한 동기화와 관련하여 고전적이고 유명한 문제로 생산자와 소비자 문제가 있다. 물건을 생산하는 프로세스인 생산자와 물건을 소비하는 프로세스인 소비자가 있다. 이 둘은 총합이라는 데이터를 공유하고 있으며, 생산자는 1을 더하고 소비자는 1을 감소시킨다.
만약 생산자가 10,000개의 물건을 생산하고 소비자가 10,000개의 물건을 소비하는 것을 동시에 실행해보면 어떤 결과가 나올까? 동기화가 이루어져있지 않다면 어떤 값이 나올지 예측할 수 없다.
생산자가 1을 더하기 위해선 총합 데이터를 복사하고, 복사한 데이터에 1을 더하고, 총합 데이터에 덮어쓴다. 소비자가 1을 빼기 위해선 총합 데이터를 복사하고, 복사한 데이터에 1을 빼고, 총합 데이터에 덮어쓴다. 즉 생산자가 총합 데이터를 복사하고 나서 소비자가 총합에서 1을 빼고, 다시 생산자가 1을 더하고 덮어쓴다면, 소비자가 뺀 값은 덮어써지면서 무효된다. 따라서 결과를 예측할 수 없다.
공유 자원과 임계 구역
공유 자원은 멀티 프로세스 환경에서 모든 프로세스가 접근할 수 있는 자원이다. 생산자와 소비자 문제의 총합 데이터가 여기에 속한다.
그리고 이런 공유 자원중에는 두 개 이상의 프로세스가 동시에 실행하면 문제가 발생하는 자원이 있다. 이런 자원에 접근하는 코드 영역을 임계 구역(critical section)이라고 한다. 임계 구역은 두 개 이상의 프로세스가 동시에 실행되면 안되는 영역이다. 두 개 이상의 프로세스가 임계 구역에 진입하고자 하면 둘 중 하나는 대기해야한다. 임계 구역에 먼저 진입한 프로세스의 작업이 마무리되면, 기다렸던 프로세스가 임계 구역에 진입한다.
하지만 잘못된 실행으로 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 경우가 있는데, 이를 레이스 컨디션(race condition)이라고 한다. 생성자와 소비자 문제가 레이스 컨디션의 사례로 볼 수 있다.
운영체제는 임계 구역 문제를 세 가지 원칙하에 해결한다. 즉, 상호 배제를 위한 동기화를 위해선 세 가지 원칙이 지켜져야 한다.
- 상호 배제(mutual exclusion) : 한 프로세스가 임계 구역에 진입했다면 다른 프로세스는 임계 구역에 들어올 수 없다.
- 진행(progress) : 임계 구역에 어떤 프로세스도 진입하지 않았따면 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야 한다.
- 유한 대기(bounded waiting) : 한 프로세스가 임계 구역에 진입하고 싶다면 그 프로세스는 언젠가는 임계 구역에 들어올 수 있어야 한다(무한정 대기해서는 안 된다).
동기화 기법
임계 구역에 오직 하나의 프로세스만 진입하게 하고, 올바른 실행순서를 보장하는 프로세스 동기화 방법은 여러가지가 있다. 그중 대표적인 도구를 알아보자
뮤텍스 락
뮤텍스 락(Mutex lock; MUTual EXclusion lock)은 임계구역에 누군가가 사용하고있는지 여부를 코드로 구현한 것이다.
임계 구역에 진입하는 프로세스는 임계 구역에 있음을 알리기 위해 뮤텍스 락을 이용해 임계구역에 접근하지 못하게 잠근다. 다른 프로세스는 임계 구역이 잠겨 있다면 기다리고, 잠겨 있지 않다면 임계 구역에 진입할 수 있다.
뮤텍스 락의 매우 단순한 형태는 하나의 전역 변수와 두 개의 함수로 구현할 수 있다.
- 자물쇠 역할 : 프로세스들이 공유하는 전역 변수 lock
- 임계 구역을 잠그는 역할 : acquire 함수
- 임계 구역의 잠금을 해제하는 역할 : release
acquire 함수는 프로세스가 임계 구역에 진입하게 호출하는 함수로, 임계구역이 열릴때까지 (lock이 false가 될때까지) 반복적으로 확인하다가 임계 구역이 열린다면 임계 구역을 잠그는(lock을 true로) 함수이다.
release 함수는 임계 구역에서의 작업이 끝나면 호출하는 함수로, 임계 구역을 열어주는 함수이다.
acquire() {
while (lock == true) // 임계 구역이 잠겨있다면
{
// 열려있는지 확인한다.
}
lock = true; // 임계 구역이 열려있다면 잠군다.
}
release() {
lock = false; // 임계 구역 작업이 끝났으니 잠금 해제
}
// 사용 예시
acquire();
// 임계 구역
release();
이렇게 되면 프로세스는 락이 열릴때까지 기다렸다가, 열리면 다시 락을 잠구고 임계 구역에 접근하여 처리를하다가, 처리를 마치면 락을 해제한다.
acquire 함수의 경우 while문을 통해 반복적으로 락이 열려있는지 체크한다. 이런 대기 방식을 바쁜 대기(busy wait)이라고 한다.
실제로 뮤텍스 락은 자체적으로 제공해주며, 위의 예제보다 더욱 정교하게 설계되어 있다.
세마포
세마포(semaphore)는 뮤텍스 락과 비슷하지만, 조금 더 일반화된 동기화 도구이다. 뮤텍스 락은 하나의 공유 자원에 접근하는 프로세스를 상정한 방법이지만, 세마포는 여러개의 공유 자원에 접근하는 프로세스를 위한 도구이다.
엄밀히 말하면 세마포의 종류도 이진 세마포(binary semaphore)와 카운팅 세마포(counting semaphore)가 있지만, 이진 세마포는 뮤텍스 락과 비슷한 개념이므로 여기에선 카운팅 세마포를 다룬다.
세마포는 뮤텍스 락과 비슷하게 하나의 변수와 두 개의 함수로 단순하게 구현할 수 있다.
- 임계 구역에 진입할 수 있는 프로세스의 개수(공유 자원의 개수)를 나타내는 전역 변수 S
- 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려주는 wait함수
- 임계 구역 앞에서 기다리는 프로세스에 ‘이제 가도 좋다’고 신호를 보내는 signal 함수
wait() {
while (S <= 0) // 자원이 없다면 대기한다.
{ }
--S; // 자원을 감소시킨다.
}
signal() {
++S; // 자원을 증가시킨다.
}
// 사용 예제
wait();
// 임계 구역
signal();
여기에선 한 가지 문제가 있다. 뮤텍스 락에도 해당되는 문제인데, 사용할 수 있는 공유 자원이 없는 경우 프로세스는 무한히 반복하여 S를 확인해야한다. 이렇게 바쁜 대기를 반복하는 것은 CPU의 주기를 낭비한다는 점에서 손해이다.
그래서 실제로 세마포는 더 좋은 방식으로 구현된다. wait 함수는 만일 사용할 수 있는 자원이 없다면, 해당 프로세스 상태를 대기 상태로 만들고, 그 프로세스의 PCB를 세마포를 위한 대기큐에 집어넣는다. 그리고 다른 프로세스가 임계 구역에서의 작업이 끝나고 signal 함수를 호출하면 signal 함수는 대기 중인 프로세스를 대기 큐에서 제거하고, 프로세스 상태를 준비 상태로 변경한 뒤 준비 큐로 옮긴다.
동기화는 상호배제도 있지만, 실행 순서 제어를 위한 동기화도 존재한다. 지금까지의 내용은 상호 배제를 위한 동기화 기법이었다면, 이제는 세마포를 이용한 실행순서 제어를 알아보자.
방법은 간단하다. 세마포의 변수 S를 0으로 두고 먼저 실행할 프로세스 뒤에 signal함수, 다음에 실행할 프로세스 앞에 wait 함수를 붙이면 된다.
이렇게 되면, P1이 먼저 실행되면 P1이 먼저 임계 구역에 진입한다. P2가 먼저 실행되더라도, wait 함수를 만나므로 P1이 임계 구역에 진입한다. 그리고 P1이 임계 구역의 실행을 끝내고 signal을 호출하면 그제서야 P2가 임계 구역에 진입한다.
모니터
세마포 자체만으로도 훌륭하지만, 사용하기엔 조금 불편한다. 매번 임계구역에 앞뒤로 wait, signal 함수를 명시해야 하는데 이것이 매우 번거로우며, 자칫 잘못 작성하여 예기치 못한 결과를 얻을수도 있다.
// 잘못된 예시 1. 세마포를 누락한 경우
// 임계 구역
// 잘못된 예시 2. wait과 signal 순서를 헷갈린 경우
signal();
// 임계 구역
wait();
// 잘못된 예시 3. wait과 signal을 중복 사용한 경우
wait();
// 임계 구역
wait();
이에 최근에 등장한 동기화 도구가 모니터(monitor)이다. 세마포에 비하면 사용자가 사용하기 훨씬 편리하다. 모니터는 공유 자원과 공유 자원에 접근하기 위한 인터페이스(통로)를 묶어 관리한다. 그리고 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근하도록 한다.
이를 위해 모니터를 통해 공유 자원에 접근하려는 프로세스를 큐에 삽입하고, 삽입된 순서대로 공유 자원을 이용하도록 한다. 즉, 모니터는 큐를 통해 하나의 프로세스만 들어오도록 하여 상호배제를 위한 동기화를 제공한다.
이 밖에도 모니터는 세마포와 마찬가지로 실행 순서 제어를 위한 동기화도 제공한다. 특정 조건을 바탕으로 프로세스를 실행하고 일시 중단하기 위해 모니터는 조건 변수(condition variable)를 사용하는데, 조건 변수는 프로세스나 스레드의 실행 순서를 제어하기 위해 사용하는 특별한 변수이다.
모니터가 조건 변수를 사용한다고는 하지만 조건 변수와 모니터는 별개의 개념이다.
조건 변수로는 wait, signal 연산을 수행할 수 있다. wait은 호출한 프로세스의 상태를 대기 상태로 전환하고 일시적으로 조건 변수에 대한 대기 큐에 삽입하는 연산이다. 모니터에 진입하기 위해 삽입되는 큐와는 다른 큐에 들어간다는 점을 유의해야한다.
wait 연산으로 일시 중징된 프로세스는 다른 프로세스의 signal 연산을 통해 실행이 재개될 수 있다. 즉, signal은 wait을 호출하여 큐에 삽입된 프로세스의 실행을 재개하는 연산이다.
모니터는 조건 변수를 이용하여 아래와 같은 프로세스 실행 순서 제어를 위한 동기화를 제공한다.
- 특정 프로세스가 아직 실행될 조건이 되지 않았을 때에는 wait를 통해 실행을 중단한다.
- 특정 프로세스가 실행될 조건이 충족되었을 때에는 signal을 통해 실행을 재개한다.
Q : 모니터란 정확히 무엇인가?
A : 모니터란 상호 배제(Mutual Exclusion) + 조건변수(Condition Variable) + 공유자원(데이터)를 하나로 묶어서, 사용자가 간편하게 쓸 수 있도록 추상화한 구조체(혹은 클래스) 형태라고 볼 수 있다.
모니터를 할 수 있도록 signal, wait을 제공하며, 상호 배제를 위한 큐를 제공해 여러개의 프로세스가 공유자원에 접근하지 못하도록 막는 기능도 제공한다. 사용자가 이 기능을 통해서 원하는 로직을 구현하면 되는데, 일반적으로 구조체 혹은 클래스에 로직을 구현하게된다.
즉, 세마포의 signal과 wait을 잘 활용하여 하나의 로직을 담고있는 구조체 혹은 클래스(이하 모니터)를 만들어뒀고, 사용자(프로그래머)는 이 모니터에서 제공하는 함수들을 사용하는 방식이라고 보면 된다(완전히 동일한 개념은 아니다).
'도서 정리 > 혼자 공부하는 컴퓨터 구조 + 운영체제' 카테고리의 다른 글
[혼자 공부하는 컴퓨터 구조 + 운영체제] 14강 가상메모리 (0) | 2025.02.16 |
---|---|
[혼자 공부하는 컴퓨터 구조 + 운영체제] 13강 교착 상태 (0) | 2025.02.16 |
[혼자 공부하는 컴퓨터 구조 + 운영체제] 11강 CPU 스케줄링 (0) | 2025.01.30 |
[혼자 공부하는 컴퓨터 구조 + 운영체제] 10강 프로세스와 스레드 (0) | 2025.01.30 |
[혼자 공부하는 컴퓨터 구조 + 운영체제] 9강 운영체제 시작하기 (0) | 2025.01.30 |