이번엔 지난 시간에 이어 유명한 프로세스 동기화와 관련된 예시를 몇 가지 살펴보려한다.
1. 생산자-소비자
중간의 원형 큐가 프로세스 A 와 프로세스 B 의 공유 버퍼(서로 접근하는 공유 메모리)라고 가정하자. 이때 A 는 항상 버퍼 큐에 데이터를 입력하고 반대로 B 는 항상 버퍼에서 데이터를 꺼내간다. 따라서 A 는 생산자, B 는 소비자의 역할이다. 우리는 프로세스 동기화에 의해 기본적으로 프로세스들의 공유 공간은 세마포어로 읽기와 쓰기가 동시에 일어나며 발생하는 데이터 불일치를 방지된다는 것을 알고 있다.
그런데 만약 A 가 데이터를 입력하려고 세마포어 자원을 획득한 상태에서 더 이상 입력할 공간이 없거나 반대로 B 가 데이터를 가져가려고 하는 순간 버퍼에 데이터가 없다면 어떻게 될까? 만약 세마포어로 위에 언급한 뮤텍스만 도입해 다수의 프로세스 접근만 막는다면 A 혹은 B 는 계속 자원을 할당 받은 상태로 빈 공간 혹은 데이터가 들어간 공간을 기다릴 것이고, 반대쪽 B 혹은 A 는 이미 상대가 자원을 할당받은 상태이니 공유 버퍼에 접근할 수 없다.
따라서 상대편이 작업을 수행해야 해결되는 상황에 상대방이 접근하지 못하도록 자원을 할당 받은 상태로 남아 있으니 A 와 B 둘 다 영원히 정상적으로 작업을 수행하지 못하며 이는 DeadLock = 교착상태가 된다.
생산자-소비자 문제를 해결하기 위해서는 위와 같이 기존의 뮤텍스와 더불어 생산자의 자원이 되는 빈 공간 그리고 소비자의 자원이 되는 찬(Full) 공간을 표현할 2개의 세마포어를 추가해야한다. 위 시퀀스를 간단하게 설명하자면 먼저 생산자인 프로세스 A 가 공유 버퍼에 데이터를 입력하려는 상황이다. 이때 생산 자원(빈 공간)이 있는지 먼저 확인한다. 생산 자원을 획득하기 전까지 프로세스 A 는 자원을 할당 받지 못한 상태로 대기하며 그동안에는 소비자인 프로세스 B 가 공유 버퍼에 접근이 가능하다. 만약 공유 버퍼에 빈 공간(생산 자원)이 존재하면 자원을 할당 받고 이후 공유 버퍼 뮤텍스를 할당 받는다. 이 과정은 위의 세마포어 연산인 P(A), P(M) 으로 간단히 구현가능하며, 이어서 작업을 완료하게 되면 먼저 뮤텍스를 반납하고 공유 버퍼에 데이터를 입력만 했기 때문에 작업을 완료했다고 해서 생산 자원인 세마포어를 반납하지 않는다. 따라서 생산자의 자원인 세마포어 A 를 반납하는게 아닌 소비자의 세마포어 B 를 증가시킨다.
소비자 또한 마찬가지로 작업 완료 시 소비 자원인 세마포어 B 가 아닌 세마포어 A 를 증가시킴으로써 결과적으로 생산자 소비자 문제를 해결할 수 있다.
2. Reader-Writer
Reader-Writer 문제는 교착상태가 발생하는 프로세스 동기화 문제는 아니다. 하지만 많은 프로세스가 하나의 공유 프로세스에 접근하는 경우에 기본적인 뮤텍스만 도입할 경우 하나의 프로세스가 작업을 진행하는 동안 수 많은 다른 프로세스들은 작업을 진행하지 못하고 비효율적으로 전체 대기시간이 증가하게 된다.
하지만 한 번 생각해보자.
프로세스의 메모리 접근 시 동작은 간단하게 주소 공간의 데이터를 가져오는 Read 그리고 주소 공간의 데이터를 수정하는 Write 뿐이다. Write 의 경우엔 분명히 동시에 둘 이상의 프로세스가 같은 공유 데이터를 수정할 경우 데이터의 불일치가 발생한다. 그런데 Read 는 둘 이상의 프로세스가 동시에 수행한다고 해서 문제가 될까? 아니다. 그저 동일한 주소 공간의 데이터를 읽을 뿐이니 모든 프로세스에 문제 없이 데이터를 읽을 수 있다. 따라서 Read-Writer 문제는 Read 시 에는 다수 접근 가능하도록 그리고 Write 의 경우에는 하나의 프로세스만 접근 가능도록 설계해야한다.
먼저 Writer 프로세스의 경우에는 기본 뮤텍스 프로세스 동기화와 마찬가지로 Write 중에는 다른 어떠한 프로세스도 접하지 못하도록 한다.
Reader 의 경우에는 공유 변수를 추가로 사용해 현재 Reader 프로세스의 수를 확인한다. 일단 공유 변수에 접근하는 것 또한 Write 이기에 공유 변수를 위한 뮤텍스 R 을 할당받으면 진행하며 read_cnt 를 1 증가시킨다. 이후 최초 Reader 확인 후 자신이 최초라면 현재 공유 메모리에 접근 중인 프로세스가 있는지 확인하고 (다른 Reader 는 없을테니 Witer 접근 확인) 뮤텍스를 할당 받는다. 이후 작업을 진행하며 최초 이후 Reader 들은 DB 뮤텍스 확인 없이 read_cnt 만 수정하며 작업을 진행하게 되어 결과적으로 여러 Reader 가 동시에 공유 데이터에 접근할 수 있다. 이후 작업을 완료하면 read_cnt 를 감소시키고 공유 데이터에 접근 중인 Reader 가 없을 경우 Writer 가 공유 데이터 접근할 수 있게 DB 뮤텍스를 반납한다.
하지만 위와 같은 방법으로 어느 정도 Reader 의 비효율적인 대기 시간을 줄일 수 있지만, Writer 에 대해 기아 문제가 발생할 수 있기 때문에 상대적으로 Write 보다 Read 가 많이 발생하는 환경에서 유리하며 기존 프로세스 간 Context Switch 오버헤드를 줄일 수 있다.
3. 식사하는 철학자
그림과 같이 원탁에 5명의 철학자가 각자 음식을 앞에두고 식사를 하려고 한다. 그런데 식사를 하기 위해서는 젓가락 2개가 필요하지만 원탁에는 위와 같이 1명의 철학자를 기준으로 양 옆으로 1개씩 밖에 없다. 모든 철학자들은 정상적으로 식사를 할 수 있을까?
프로세스에 비유한다면 다수의 프로세스가 최대 5개의 자원을 가진 세마포어를 2개 얻어야 작업이 가능한데 모두 1개씩 만 할당 받아 교착 상태(DeadLock)에 걸린 것으로 볼 수 있다. 따라서 우리는 단순하게 한 명의 철학자가 2개의 젓가락을 가져가는 것이 아닌 새로운 해결책을 찾아야한다.
1. 작업이 들어오면 철학자가 젓가락 들기를 시도한다.
젓가락 들기 {
현재 상태를 배고픔(젓가락이 필요한 상태)으로 변경
양옆 철학자가 식사 중 이지 않고 내가 배고픈 상태면 나를 식사 중(젓가락 사용 중)으로 변경 및 철학자 상태 Wake Up
먹는 중이 아니라면 철학자 상태를 Wait 으로 변경
}
2. 작업을 완료하면 젓가락 놓기를 시도한다.
젓가락 놓기 {
생각 중(젓가락 필요 없는 상태)으로 상태 변경
왼쪽 철학자 배고픈 상태면 식사
오른족 철학자 배고픈 상태면 식사
}
위 형태는 세마포어가 아닌 모니터라는 개념에 더 가까운 설계로 프로세스 동기화를 만족해주며 추가로 하나하나 절차를 개발자가 작성하는 것이 아닌 기능을 캡슐화하여 객체 지향적으로 설계가 가능하게 해준다. 대부분의 멀티 프로세서 환경에서 모니터 구조체를 지원해주며 세마포어 보다 쉽게 코드 설계가 가능하고 캡슐화 된 함수를 사용함으로써 보다 개발 간 오류를 줄일 수 있게 해준다.