프로세스 동기화
프로세스 동기화(Process synchronization)란 프로세스 사이에 동기화를 하는 것을 말한다. 현재는 대부분 쓰레드 기준으로 문맥 교환(Context Switching) 이 일어나기 때문에 쓰레드 동기화(Thread synchronization)
라고도 불린다.
프로세스 동기화는 여러 프로세스가 공유하는 자원의 일관성을 유지
하는 것이다. 그럼 동기화(Synchronization)는 공유 자원의 일관성을 유지
하는 것으로 볼 수 있다.
프로세스 동기화에 대한 시작은 경쟁 상태(Race Condition)
와 임계 구역(Critical Section)
에 대한 이해부터 시작된다.
경쟁 상태(Race Condition)
경쟁 상태(Race Condition)란 동시에 여러 개의 프로세스 혹은 쓰레드가 동일한 자료를 접근하여 조작하고, 그 실행 결과가 접근이 발생한 특정 순서에 의존하는 상황을 말한다.
쉽게 말하면, 다수의 프로세스 혹은 쓰레드가 동기화 없이 공유 자원에 접근하려는 현상을 의미한다. 따라서 경쟁 상태가 발생되면, 프로세스와 스레드에서 배웠던 동시성 이슈가 발생할 수 있다.
이러한 경쟁 상태를 해결하기 위해서 동기화(Synchronization)
가 필요한 것이다.
데이터베이스에서의 경쟁 상태
데이터베이스를 사용하는 경우에도 경쟁 상태라는 개념이 존재한다.
예를 들어 다음과 같은 쿼리가 있다고 가정하자.
-- 현재 PK 의 최댓값에 1을 더해 새로운 PK 로 사용
SELECT MAX(PK) + 1 AS NEXT_PK FROM USER;
이러한 방식은 두 개의 클라이언트가 동시에 쿼리를 실행할 수 있다면 안전하지 않다. 두 클라이언트에서 같은 값을 사용하게 될 수도 있기 때문이다. 이런 것을 경쟁 상태(race condition)
이라고 한다.
이러한 문제를 시퀀스(Sequence)
를 사용하여 해결할 수 있다.
시퀀스(Sequence)는 트랜잭션 범위 밖에서 동작해 이 문제를 해결한다. 시퀀스는 여러 클라이언트에 절대 같은 값을 할당하지 않고, 삽입할 행에 사용한 값을 커밋했는지 여부와 상관없이 한 번 할당한 값을 되돌리지도 못한다. 시퀀스는 이런 식으로 동작하기 때문에, 여러 클라이언트가 동시에 유일한 값을 할당받을 수 있고 중복된 값을 할당 받지 않는다고 확신할 수 있다.
다른 클라이언트가 동시에 자신이 사용할 값을 생성하더라도, 시퀀스가 생성한 마지막 값을 확인할 수 있는 함수는 현재 세션에서 생성한 마지막 값을 리턴하므로 경쟁 상태가 없다.
다시 돌아와서 임계 구역에 대해서 살펴보자.
임계 구역(Critical Section)
우리는 멀티 쓰레드 환경에서 동시성 이슈가 일어날 수 있다는 것을 배웠다. 공유 자원의 일관성을 유지시키기 위해서는 공통 변수에 접근하는 쓰레드가 하나만 존재하도록 관리해야 하는데, 이 공통 변수를 변경하는 코드의 영역을 임계 구역(Critical Section)
이라고 한다.
임계 구역에 대한 문제를 해결하기 위해서 세가지 요구조건을 충족해야 한다. 임계 구역에 대한 문제란, 임계 구역을 어떻게 보호하여 공유 자원의 일관성을 유지시킬 것인가에 대한 문제라고 보면 된다.
- 상호 배제(Mutual exclusion)
- 한 쓰레드가 임계 구역에서 실행 중이라면, 다른 쓰레드는 임계 구역에 접근할 수 없다.
- 진행(Progress)
- 한 임계구역에 접근하는 쓰레드를 결정하는 것은 유한 시간 이내에 이루어져야한다.
- 한정된 대기(Bounded waiting)
- 임계구역으로 진입하기 위해 대기하는 모든 쓰레드는 유한 시간 이내에 해당 임계구역으로 진입할 수 있어야 한다.
Mutex Locks
임계 구역을 보호하고 경쟁 상태를 방지하기 위한 해결하기 위한 방법으로 Mutex Locks
가 있다.
프로세스/쓰레드는 임계 구역에 들어가기 전에 반드시 락을 획득해야 하고, 임계 구역을 빠져 나올 때 락을 반환해야 한다.
- Mutex Locks
- acquire() 함수로 락을 획득, release() 함수로 락을 반환
- Mutex 락은
available
이라는 boolean 변수를 가지는데, 이 변수 값이 락의 가용 여부를 표시한다. 락이 가용하면 acquire() 호출은 성공하면서 락은 사용 불가 상태가 된다.
- Mutext Locks 를 사용한 임계 구역 문제 해결 방안
do {
acquire(); // 락을 획득 : entry section
critical section // 임계 구역
release(); // 락을 반환 : exit section
remainder section // 나머지 구역
} while(true);
acquire() {
while(!available)
; /* busy wait */
available = false;
}
release() {
available = true;
}
Mutex Locks 를 통해서 임계 구역 문제를 해결할 수 있지만, 바쁜 대기(busy waiting)
이라는 방식을 사용한다는 단점이 존재한다. 바쁜 대기란 한 프로세스가 자원을 사용 중이라면, 락이 사용 가능해질 때 까지 대기하는 방식을 의미한다.
하나의 프로세스/스레드가 자원을 사용 중이라면 다른 자원들은 락이 사용 가능해질 때까지 기다리면서 계속 회전하고 있기 때문에 spinlock
이라고도 부른다. 대신 장점은 락을 기다리는 동안 문맥 교환이 발생되지 않기 때문에 CPU 성능을 올릴 수 있다는 것이다. 단, 프로세스들이 짧은 시간 동안만 락을 소유한다고 하면 spinlock(Mutext Locks)
이 유용하다.
Mutex 는 Mutual exclusion 의 축약 형태이다.
세마포(Semaphores)
세마포(Semaphores)는 Mutex Locsk 와 유사하게 동작하지만 더 정교한 동기화 도구라고 할 수 있다.
세마포 S
는 정수 변수로서, wait() 과 signal() 함수로만 접근이 가능하다. wait() 은 '검사하다'를 의미하는 네덜란드 단어 Proberen 에서 P
를 따왔고, signal() 연산은 '증가하다'를 의미하는 Verhogen 에서 V
를 따왔다.
S
: 세마포의 정수 값P
: wait()V
: signal()
아래의 코드는 바쁜 대기를 하는 세마포
코드이다.
wait(S) {
while(S <= 0)
; /* busy wait */
S--;
}
signal(S) {
S++;
}
세마포는 일반적으로 상호 배제(Mutual exclusion)
를 지키기 위해 사용된다.
한 쓰레드가 세마포 값을 변경하면, 다른 쓰레드는 동시에 동일한 세마포 값을 변경할 수 없다. 세마포는 이진 세마포(binary semaphores)
와 카운팅 세마포(counting semaphores)
가 존재한다.
이진 세마포는 말 그대로 0과 1의 값만 가지기 때문에 Mutex Locks 와 유사하게 동작한다. 카운팅 세마포는 위 코드와 동일하게 동작한다. 즉, 자원을 사용하려는 프로세스는 wait() 연산을 수행하여 세마포 값을 감소시키고, 자원을 방출 시킬 때 signal() 연산을 통해서 세마포 값을 증가시킨다. 세마포 값이 0이 되면 모든 자원이 사용 중임을 나타낸다. 즉, 세마포 값이 0인 상태에서는 다른 프로세스가 자원을 사용하려고 해도 세마포 값이 증가할 때 까지 기다려야 한다.
위 코드를 보면 알겠지만 세마포 역시 busy wait
문제가 존재한다.
세마포의 값을 대기하면서 봉쇄된 프로세스는 다른 프로세스가 signal() 연산을 실행하면 wakeup() 연산에 의해서 재시작 된다.
wakeup()
연산은 대기 상태에서 준비 상태로 변경시킨다.
준비 상태로 변경된 프로세스는 준비 리스트
(준비 완료 큐, 보통 리스트는 큐로 되어있다.)에 들어간다.
세마포 구현
세마포를 구현하면 다음과 같다.
typedef struct {
int value;
struct process *list;
} semaphore;
각 세마포는 한 개의 정수 value 와 프로세스 리스트(list)를 가진다. 프로세스가 세마포를 기다려야 하면, 이 프로세스는 list 에 추가된다.
아래 코드에서 processPriorityQueue
로 표기한 이유는 다음과 같다. 준비 리스트는 보통 큐로 구현되어있는데, 자바로 따지면 우선순위큐로 표현하는게 좋을 것 같다라고 생각했다. 왜냐하면
프로세스 상태 전이를 배우면서 준비 리스트에 대한 설명이 다음과 같았던 것을 알 수있다.
- 준비 리스트 : 각각 우선순위를 부여해서 우선순위가 높은 프로세스가 다음 순서에 CPU 를 할당 받는다.
따라서, 자바화를 한다면 우선순위를 부여해야 하니까 PriorityQueue 가 이해가 더 편할 것 같아서 아래와 같이 작성하였다.
void wait(semaphore *S) {
S -> value--;
if(S -> value < 0) {
processPriorityQueue.offer(this.process);
block();
}
}
block()
연산은 실행
상태에서 대기
상태로 변경 시킨다.
signal() 연산은 프로세스 리스트에서 한 프로세스를 꺼낸다.
void signal(semaphore *S) {
S -> value++;
if(S -> value <= 0) {
Process process = processPriorityQueue.poll();
wakeup(process);
}
}
wakeup()
연산은 대기
상태에서 준비
상태로 변경시킨다. 세마포 값이 음수를 가질 때 그 절대 값은 세마포를 대기하고 있는 프로세스들의 수이다.
각 세마포는 정수 값과, 프로세스 제어 블록(PCB) 리스트에 대한 포인터를 갖고 있다. 한정된 대기(Bounded waiting)
를 보장하도록 리스트에 프로세스를 추가하고 삭제하는 방식으로 큐(Queue)
를 사용하는 것이다.
세마포는 원자적으로 실행되도록 해야 한다. 즉, wait() 과 signal() 을 동시에 실행할 수 없도록 해야 한다.
"바쁜 대기를 하는 세마포" 의 코드와 달리 wait() 연산에서 세마포 값의 감소랑 검사 로직 순서가 바뀌어서 음수값이 가능하다. 코드가 다르다고해서 바쁜 대기(busy wait) 문제가 아예 해결된 것은 아니다. 코드와 위 설명을 보면 충분히 이해할 것이다. 대신 Mutex Locks 에서는 busy wait 이 entry section 부터 일어났다면, 세마포에서는 wait() 과 signal() 의 critical section 에만 국한시켰다. 사실상 임계 구역에 대한 코드는 큰 로직이 들어있는것도 아니고 짧기 때문에 세마포에서는 바쁜 대기(busy wait)가 드물게 발생하고, 발생하더라도 그 시간이 짧다고 한다.
다음으로
이어서 공부하면 좋은 내용은 교착 상태(Deadlock)와 기아(Starvation)이다.
References
'CS > OS' 카테고리의 다른 글
스와핑 (0) | 2021.12.17 |
---|---|
주소 바인딩 (0) | 2021.12.17 |
교착상태와 기아상태 (0) | 2021.12.15 |
Interrupt 와 Context Switching (0) | 2021.12.09 |
프로세스와 쓰레드 (0) | 2021.12.09 |