본문 바로가기
KNOU CS/운영체제

[운영체제] 병행 프로세스 - 세마포어 완벽 정리 Part 3 - 판독기-기록기 문제

by 보눔비스타 2025. 3. 19.

지난 두 편의 글을 통해 세마포어의 정의와, '상호배제 문제'와 '동기화 문제', '생산자-소비자 문제'를 세마포어가 어떻게 해결하는지에 대해 알아보았다. 이번 포스팅은 세마포어 관련 마지막 포스팅으로, 세마포어로 해결할 수 있는 문제 중 판독기-기록기 문제를 집중적으로 살펴보도록 한다. 

판독기-기록기 문제란? 

판독기-기록기 문제는 다수의 협력 프로세스가 공유 자원을 사이에 두고 데이터를 읽는 판독기(reader)와 데이터를 쓰는 기록기(writer)로 나위어 동작할 때, 이들의 접근을 어떻게 제어할 것인지에 대한 문제다. 이 문제의 핵심은 두 가지 조건을 만족시키는 것이다. 

  1. 기록기의 상호 배제: 한 기록기가 공유 자원에 데이터를 쓰는 동안 다른 기록기나 판독기는 접근할 수 없음. 데이터를 쓰고 있는 중에 누가 읽으려고 하거나 또 다른 데이터를 쓸 수 없다는 것. 
  2. 판독기의 병행성: 여러 판독기는 동시에 공유 자원에서 데이터를 읽을 수 있음. 읽기만 하는 작업은 데이터를 변경하지 않으므로 서로 병행 수행이 가능함. 단, 판독기가 읽는 동안에는 기록기가 접근할 수 없음.

판독기-기록기 문제는 판독기에 우선순위를 부여하느냐, 기록기에 우선순위를 부여하느냐에 따라 제1판독기-기록기 문제제2판독기-기록기 문제로 나뉘게 된다. 

 

제1판독기-기록기 문제: 판독기 우선

제1판독기-기록기 문제는 판독기에 우선순위를 부여하는 방식이다. 즉, 누군가가 공유 자원을 읽고 있을 때, 새로운 판독기가 읽으려고 접근한다면 바로 들어와서 같이 읽을 수 있다. 이때 먼저 대기 중인 기록기가 있더라도, 새로운 판독기에게 우선권이 있기 때문에 기록기는 대기시키고 판독기를 즉시 자원에 접근시킬 수 있다. 이 방식은 판독기-기록기 문제의 두 가지 핵심 조건 중 '판독기의 병행성' 조건을 만족시킨다.

  • 장점: 판독기들이 동시에 자유롭게 읽을 수 있기 때문에 읽기 작업이 많은 시스템에서 빠르고 효율적이다. 
  • 단점: 기록기가 너무 오래 기다릴 수 있다. 예를 들어 한 판독기가 읽고 있는데 또 다른 판독기가 오고, 다른 판독기가 또 오고, 계속 반복되면 기록기는 자원을 쓸 기회를 못 얻고 계속 기다려야 한다. 이것을 기아 상태(starvation)이라고 부른다.  

세마포어로 해결하기

세마포어 설정

제1판독기-기록기 문제에서는 세 가지 변수를 사용한다.

  • wrt : 기록기와 판독기가 동시에 자원을 못 쓰게 막는 세마포어(초기값 1, 즉 문이 열려 있음).
  • mutex : 판독기 수를 세는 rcount라는 변수를 안전하게 바꾸기 위한 세마포어(초기값 1).
  • rcount : 지금 자원을 읽고 있는 판독기가 몇 명인지 세는 변수(초기값 0).

코드 구조

  • 기록기 코드
P(wrt);           // 문을 잠가서 자원을 독점
공유자원에 쓰기;  // 데이터 수정
V(wrt);           // 다 썼으니 문 열기

 

기록기 코드는 단순하다. wrt를 잠갔다가 쓰기를 끝내면 열어준다. 이 동안 다른 기록기나 판독기는 못 들어온다.

 

  • 판독기 코드
P(mutex);         // rcount를 바꾸기 위해 잠시 문 잠금
rcount = rcount + 1; // 나도 읽으러 왔으니 숫자 1 추가
if (rcount == 1) then P(wrt); // 내가 첫 번째라면 기록기 막기 위해 문 잠금
V(mutex);         // rcount 바꾸기 끝, 문 열기
공유자원에서 읽기; // 데이터 읽기
P(mutex);         // rcount 줄이기 위해 다시 문 잠금
rcount = rcount - 1; // 읽기 끝났으니 숫자 1 빼기
if (rcount == 0) then V(wrt); // 내가 마지막이라면 기록기 풀어주기
V(mutex);         // rcount 바꾸기 끝, 문 열기
  • 판독기는 rcount를 보고 지금 읽고 있는 판독기가 몇 개인지 확인한다.
  • 첫 번째 판독기(rcount == 1)라면 wrt를 잠가서 기록기가 못 들어오게 한다.
  • 이미 다른 판독기가 읽고 있다면(rcount > 1) 그냥 같이 읽기 시작하면 된다.
  • 읽기가 끝나면 rcount를 줄이고, 마지막 판독기라면(rcount == 0) wrt를 풀어서 기록기가 들어올 수 있게 한다.
  • mutex는 rcount를 바꿀 때 다른 판독기가 동시에 건드리지 않도록 보호하는 역할을 한다.

rcount를 바꿀 때 P(mutex)로 문을 잠가야만 하는 이유

rcount는 현재 공유 자원을 읽고 있는 판독기의 수를 세는 변수다. 판독기가 자원을 읽기 시작할 때 rcount를 1 늘리고, 읽기를 끝내면 1 줄인다. 이 값이 중요한 이유는,

  • rcount == 1일 때 첫 번째 판독기가 기록기를 막기 위해 P(wrt)로 문을 잠갔다가,
  • rcount == 0일 때 마지막 판독기가 V(wrt)로 문을 열어줘서 기록기가 자원을 쓸 수 있게 하기 때문.

이 과정에서 문제가 생길 수 있는 상황을 예로 들어보자. 만약 두 판독기가 거의 동시에 자원을 읽으러 온다고 가정해보자. 세마포어로 문을 잠그지 않으면 다음과 같이 꼬일 수 있다.

  1. 첫 번째 판독기: rcount를 읽어보니 0이네? 그럼 1로 바꿔야지. (아직 바꾸기 전)
  2. 두 번째 판독기: 나도 rcount를 읽어보니 0이네? 그럼 1로 바꿔야지
  3. 첫 번째 판독기: rcount를 1로 설정.
  4. 두 번째 판독기: 나도 rcount를 1로 설정.

이제 rcount는 2가 되어야 하는데, 두 판독기가 동시에 읽고 덮어쓴 탓에 1로 남아버린다. (경쟁 조건이 됨) 이렇게 되면:

  • 첫 번째 판독기만 wrt를 잠갔다고 생각하고, 두 번째 판독기는 잠그지 않아서 기록기가 끼어들 수 있게 된다. 그러면 데이터가 엉망이 될 수 있다.
  • 나중에 두 판독기가 끝날 때 rcount가 0이 안 되니까 wrt가 풀리지 않아 기록기가 영원히 기다릴 수도 있다.

즉, P(mutex)로 문을 잠그는 이유는 다른 판독기가 동시에 rcount를 건드리지 못하는 것이고, 작업이 끝나면 V(mutex)로 문을 열어서 다음 판독기가 들어올 수 있게 해주는 것이다. 

 

이렇게 하면,

  1. 첫 번째 판독기가 rcount를 0에서 1로 바꾸고 wrt를 잠갔다가 나갈 때까지
  2. 두 번째 판독기는 P(mutex)에서 기다린다.
  3. 첫 번째 판독기가 끝나면 두 번째 판독기가 들어와서 rcount를 1에서 2로 바꾼다. 

결과적으로 rcount는 정확히 2가 되고, 경쟁 조건이 사라져서 시스템이 제대로 돌아가게 된다. 

 

제2판독기-기록기 문제: 기록기 우선

제2판독기-기록기 문제는 기록기에 우선순위를 부여하는 방식이다. 판독기가 공유 자원을 읽고 있어도, 기록기가 기다리고 있다면 새로 온 판독기는 기다리고 기록기가 먼저 쓰기를 끝내도록 한다. 이 방식은 기록기가 너무 오래 기다리지 않게 하려는 해결책이다. 

  • 장점: 기록기가 기아 상태에 빠지지 않기 때문에 쓰기 작업이 꼭 필요한 시스템에서 유용하다.
  • 단점: 판독기들이 동시에 읽을 수 있는 기회가 줄어든다. 또 기록기를 너무 우선하다 보면 판독기가 오래 기다릴 수도 있다.

세마포어로 해결하기

세마포어 설정

기록기를 보호하기 위해 더 많은 세마포어가 필요하다. 사용하는 변수는 다음과 같다. 

  • rd: 판독기가 들어오는 걸 제어하는 세마포어(초기값 1).
  • wrt: 기록기 상호 배제를 위한 세마포어(초기값 1).
  • mutex1: rcount를 안전하게 바꾸기 위한 세마포어(초기값 1).
  • mutex2: wcount를 안전하게 바꾸기 위한 세마포어(초기값 1).
  • mutex3: 판독기 순서를 조절하는 세마포어(초기값 1).
  • rcount: 읽고 있는 판독기 수(초기값 0).
  • wcount: 쓰고 있는 기록기 수(초기값 0).

코드 구조

  • 기록기 코드
P(mutex2);        // wcount 바꾸기 위해 문 잠금
wcount = wcount + 1; // 나도 쓰러 왔으니 숫자 1 추가
if (wcount == 1) then P(rd); // 내가 첫 번째라면 판독기 막기
V(mutex2);        // wcount 바꾸기 끝
P(wrt);           // 자원 쓰기 시작
공유자원에 쓰기;
V(wrt);           // 쓰기 끝
P(mutex2);        // wcount 줄이기 위해 문 잠금
wcount = wcount - 1; // 다 썼으니 숫자 1 빼기
if (wcount == 0) then V(rd); // 내가 마지막이라면 판독기 풀기
V(mutex2);        // wcount 바꾸기 끝

 

기록기 동작 과정을 살펴보면, 

 

1. 시작 : 기록기 수 세기

  • P(mutex2): wcount라는 변수를 바꾸기 전에 문을 잠근다. wcount는 현재 자원을 쓰고 있는 기록기 수를 세는 변수이므로, 여러 기록기가 동시에 건드리지 않게 보호한다.
  • wcount = wcount + 1: "나도 쓰기 시작할게" 하면서 기록기 수를 1 늘린다. 
  • if (wcount == 1) then P(rd): 첫 번째 기록기라면(wcount == 1), rd 문을 잠가서 새 판독기가 들어오지 못하게 막는다. 이것이 기록기 우선 방식의 핵심이다.
  • V(mutex2): wcount를 다 바꿨으니 이제 문을 연다. 이제 다른 기록기가 필요하면 wcount를 바꿀 수 있다.

2. 자원에 쓰기

  • P(wrt): 이제 공유 자원을 쓸 차례라 wrt 문을 잠가서 다른 기록기나 판독기가 끼어들지 못하게 한다. (자원 독점)
  • 공유자원에 쓰기: 실제로 데이터를 수정한다. 이 부분이 안전하게 단독으로 실행된다.
  • V(wrt): 쓰기가 끝났으니 wrt 문을 풀어서 자원을 다시 쓸 수 있게 한다.

3. 끝 : 기록기 수 줄이기

  • P(mutex2): 다시 wcount를 바꾸기 위해 문을 잠근다.
  • wcount = wcount - 1: "나 쓰기 끝났어" 하면서 기록기 수를 1 줄인다.
  • if (wcount == 0) V(rd): 마지막 기록기라면(wcount == 0), rd 문을 풀어서 대기 중인 판독기가 이제 들어올 수 있게 한다.
  • V(mutex2): wcount 수정이 끝났으니 이제 문을 연다.
  • 판독기 코드
P(mutex3);        // 순서 맞추기 위해 문 잠금
P(rd);            // 기록기가 기다리는지 확인
P(mutex1);        // rcount 바꾸기 위해 문 잠금
rcount = rcount + 1; // 나도 읽으러 왔으니 숫자 1 추가
if (rcount == 1) then P(wrt); // 내가 첫 번째라면 기록기 막기
V(mutex1);        // rcount 바꾸기 끝
V(rd);            // 다음 판독기 들어오게 풀기
V(mutex3);        // 순서 조절 끝
공유자원에서 읽기;
P(mutex1);        // rcount 줄이기 위해 문 잠금
rcount = rcount - 1; // 읽기 끝났으니 숫자 1 빼기
if (rcount == 0) then V(wrt); // 내가 마지막이라면 기록기 풀기
V(mutex1);        // rcount 바꾸기 끝

 

판독기 동작 과정을 살펴보면,

1. 시작: 순서 확인과 기록기 대기 체크
  • P(mutex3): 판독기들이 순서대로 들어오도록 문을 잠근다. 이건 여러 판독기가 동시에 들어올 때 혼란을 막기 위한 장치다.
  • P(rd): 기록기가 기다리고 있는지 확인한다. 만약 기록기가 rd를 잠갔다면 여기서 대기하게 되고, 이것이 기록기 우선 방식을 구현하는 핵심이다.
  • P(mutex1): rcount라는 변수를 바꾸기 위해 문을 잠근다. rcount는 읽고 있는 판독기 수를 세는 변수라, 동시에 건드리면 안 된다.
  • rcount = rcount + 1: "나도 읽기 시작할게" 하면서 판독기 수를 1 늘린다.
  • if (rcount == 1) then P(wrt): 첫 번째 판독기라면(rcount == 1), wrt를 잠가서 기록기가 자원을 못 쓰게 막는다.
  • V(mutex1): rcount 수정을 끝냈으니 문을 연다.
  • V(rd): rd를 열어서 다음 판독기가 들어올 수 있게 한다. 기록기가 기다리고 있지 않으면 바로 진행된다.
  • V(mutex3): 순서 조절이 끝났으니 문을 연다.
2. 자원 읽기
  • 공유자원에서 읽기: 이제 자원을 읽는다. 여러 판독기가 동시에 읽을 수 있지만, 기록기는 wrt 때문에 못 들어온다.

3. 끝 : 판독기 수 줄이기

  • P(mutex1): 다시 rcount를 바꾸기 위해 문을 잠근다.
  • rcount = rcount - 1: "나 읽기 끝났어" 하면서 판독기 수를 1 줄인다. 
  • if (rcount == 0) then V(wrt): 마지막 판독기라면(rcount == 0), wrt를 열어서 대기 중인 기록기가 자원을 쓸 수 있게 한다.
  • V(mutex1): rcount 수정이 끝났으니 문을 연다.