본문 바로가기
운영체제

운영체제 9장 - 프로세스 관리(6) : 생산자-소비자 문제 -

by ChocoPeanut 2017. 4. 20.

운영체제 9

- 프로세스 관리(6) : 생산자-소비자 문제 -

 

프로세스 동기화는 프로세스 관리 분야에서 중요한 분야이다. 앞의 장들은 동기화를 공부하기 위해 은행계좌 문제를 예시로 들어서 설명하였다. 이번 장은 대표적인 프로세스 동기화에 대한 문제인 생산자-소비자 문제를 설명 할 것이다.


생산자-소비자 문제는 생산자가 데이터를 생상하면 소비자는 그것을 소비하는 형태에서 발생하는 문제를 말한다. 컴퓨터 세계에서 예를 들면 웹 서버와 웹 클라이언트로 들 수 있다. 웹 서버가 데이터를 생산하여 웹에 관련되어 보여주는 작업들을 수행하고 웹 클라이언트는 웹 주소로 접속해 화면을 통해 보게 되는 형태의 소비 작용을 한다.


일반적으로 생산하는 속도와 소비하는 속도에 차이가 존재한다. 실제로 생산되는 속도가 소비하는 속도보다 빠른 경우가 많아서 생산된 데이터는 바로 소비되지 못한다. 이를 보안하기 위해 생산된 데이터를 보관하는 버퍼라는 공간이 존재한다. 생산자가 데이터를 생산하면 버퍼에 보관을 하게 되고 소비자는 버퍼에서 데이터를 빼내어 사용한다. 하지만 현실 시스템에는 버퍼의 크기가 유한하다. 크기가 정해져 있는 버퍼이기 때문에 Bounded Buffer라고 불린다. 생산자는 버퍼가 가득 차면 더 이상 넣을 수가 없다. 반대로 소비자는 버퍼가 비면 뺄 수가 없다.


위와 같은 문제의 키워드를 가지고 자바 코드를 이용해서 작성을 해보자.


우선 버퍼에 대한 클래스를 만들어 버퍼를 정수 값으로 지정을 하였다. size는 버퍼의 크기를 나타내고 count는 버퍼에 들어와 있는 데이터의 개수라고 할 수 있다. 생산된 데이터가 버퍼로 들어오면 count1증가시키고 소비자가 빼어 가면 1감소시킨다. in은 데이터를 넣는 위치를 out은 빼내는 위치를 가리킨다. in 값은 데이터가 들어올 때마다 1씩 증가하고 out 값은 데이터가 나갈 때마다 1씩 증가한다. 만약 in의 값이 size까지 가게 되면 다시 처음 0의 값으로 돌아가서 데이터를 넣기 시작한다. 버퍼가 가득 차여져 있으면 out이 실행되기 전까지 무한루프를 돌면서 데이터를 넣지 못하고 기다리고 있는 상태가 된다. 반대로 버퍼가 비어져 있으면 out은 실행되지 못하고 무한루프를 돌게 되고 in으로 데이터가 들어오기를 기다리게 된다.



생산자와 소비자는 서로 다른 동작을 하는 쓰레드로 제작을 하면 된다. 생산자의 쓰레드의 경우는 N번 데이터를 생산하여 버퍼에 넣어주는 작업을 소비자의 쓰레드는 N번 데이터를 버퍼에서 가져와 소비하는 작업을 만들어 준다. 메인함수에서는 100크기의 버퍼를 만들고 N을 만 번으로 지정한다.



위의 코드를 실행하게 되면 당연히 만 번 데이터가 들어가고 만 번 데이터가 나와 결과적으로 버퍼의 count0이 되어야 하는데 실행을 해보면 그렇게 되지 않는 경우가 발생하는 것을 알 수 있다. 또한 실행이 불가한 경우도 발생한다. 이러한 문제가 발생하는 이유가 바로 쓰레드 동기화가 되지 않았기 때문이다. 공통변수인 count, buf에 대한 동시 업데이트가 발생하여 값의 변화에 문제가 생긴다. 공통변수 업데이트 구간인 임계구역에 대한 동시 진입이 가능하므로 데이터의 변화가 따로 발생할 수 있으므로 문제가 발생한다. 다시 말해 생산자가 count를 올리고 있는 도중에 문맥전환에 의해 소비자가 돌게 되어 count를 낮추는 작업을 하게 되면 count의 값에 오류가 발생하는 것이다.


이를 해결하기 위해서는 임계구역에 대한 동시 접근을 방지하는 상호배타의 기능이 추가되어야한다. 우리는 앞장을 통해 상호배타를 적용시킬 수 있는 방법을 알고 있다. 바로 세마포를 이용하여 상호배타를 적용시켜 주는 것이다. 세마포의 정수 값을 1로 두어 생산자나 소비자 중 하나만이 임계구역에 들어갈 수 있도록 해주어야 한다. 이렇게 세마포를 만들게 되면 생산자가 데이터를 생산하여 버퍼에 넣어주는 작업을 수행하게 되면 소비자는 데이터를 빼내는 작업을 할 수 없게 된다. 공통된 값인 countbuf에 관련된 업데이트가 동시에 되지 않으므로 오류가 발생하지 않는다.


자바 코드에서 수정을 하는 부분은 insertremove 공간에서 버퍼의 데이터를 업데이트 해주는 부분을 감싸는 acquire()명령어와 release()명령어를 입력해주면 된다. 물론 세마포에 대한 정의를 따로 해주어야 한다. 우리는 이렇게 세마포를 사용하여 프로세스/쓰레드 동기화를 시켜주어 오류를 수정해 줄 수 있다.




하지만 위의 과정은 오류에 대한 수정은 하였지만 기다려야하는 시간이 길어질 수 있다. 앞의 키워드에서 주어졌듯이 생산자는 버퍼가 가득 차 있으면 기다려야하고 소비자는 버퍼가 비어 있으면 기다려야한다. 코드를 보면 무한루프를 돌면서 계속 CPU가 할당되어 잡혀있는데 이런 시간은 매우 좋지 않다. 이런 기다림을 Busy-wait이라고 한다. CPU가 아무 일도 하지 않게 되면 성능이 매우 낭비가 된다. 이를 해결하는 것도 세마포를 이용해서 할 수 있다. 세마포의 장점 중 하나는 프로세스나 쓰레드의 실행 순서를 사용자가 원하는 순서에 따라 작업을 수행시킬 수 있게 하는 것이다. 만약 버퍼가 가득 차여 있으면 세마포의 공간에 생산자를 가두어 CPU가 할당 받지 못하도록 한다. 이렇게 되면 CPU는 무조건 소비자에게 CPU를 할당할 수밖에 없어 버퍼의 빈 공간을 만들어 주게 된다. 반대로 버퍼가 비어있으면 CPU할당을 생산자에게 주어 버퍼를 채우게 한다.


이러한 세마포를 재작해보자. 위와 같은 동작을 하려면 세마포를 두 개를 재작해야한다. 생산자와 버퍼와의 관계를 제어할 수 있는 세마포(empty)와 소비자와 버퍼의 관계를 제어하는 세마포(full)가 필요하다. 생산자와 버퍼를 제어하는 세마포는 정수 값의 초기 값을 size 값으로 가지게 된다. 데이터가 계속 버퍼에 들어가는데 버퍼가 size만큼 들어가게 되면 생산자는 block이 되어야한다. 반대로 소비자의 세마포는 비어 있을 때 block되어야 하므로 정수 값의 초기 값으로 0을 가진다. 버퍼에 데이터가 들어오면 정수의 값을 증가시키고 데이터가 없는 빈 버퍼가 되었을 때 acquire()명령으로 데이터를 빼내오려고 한다면 정수 값이 0보다 작은 값을 가지게 되므로 소비자 쓰레드를 block 시킨다. 두 개의 세마포는 release를 명령을 보내게 되면 상대방의 세마포를 풀어주어 동작을 할 수 있게 한다. 이렇게 제어를 하게 되면 CPU가 무한 루프를 돌고 있는 작업을 하는 것을 막을 수 있다.