세마포어는 초기값에 의해 동작이 결정되기 때문에, 사용하기 전 “제일 먼저” 값을 초기화해야 한다.
3번째 인자로 1을 전달하여 세마포어의 값을 1로 초기화한다.
2번째 인자는 0인데, 같은 프로세스 내의 쓰레드 간에 세마포어를 공유한다는 것을 의미한다.
세마포어: sem_wait()과 sem_post()의 정의이다.
sem_wait()
함수는 즉시 리턴하거나(세마포어의 값이 1 이상이면) 아니면 해당 세마포어 값이 1 이상이 될 때 까지 호출자를 대기시킨다.
다수의 쓰레드들이 sem_wait()을 호출할 수 있기 때문에, 대기큐에는 다수의 쓰레드가 존재할 수 있다.(대기하는 법에는 회전과 재우기의 두 가지가 있다.)
sem_post()
함수는 대기하지 않는다.
세마포어 값을 증가시키고 대기 중인 쓰레드 중 하나를 깨운다.
세마포어가 음수라면 그 값은 현재 대기 중인 쓰레드의 개수와 같다.
이 두개의 함수는 원자적으로 실행된다고 가정한다.
1.2 이진 세마포어(락)
위의 코드는 이진 세마포어이다.
위의 그림은 이진 세마포어를 사용하는 두 개의 쓰레드의 흐름이다.
1.3 순서 보장을 위한 세마포어
자식이 끝나기를 기다리는 부모 쓰레드의 대기를 위해서 세마포어를 어떻게 사용할까
이경우에는 세마포어의 초기값을 0으로 해야한다. 부모 쓰레드가 자식 쓰레드를 생성하고 자식 쓰레드가 아직 실행하지 않을 경우에도 부모 쓰레드는 대기해야한다.
그렇기 때문에 0으로 초기화하면 부모 쓰레드는 wait을 만나자마자 -1로 인해 대기된다. 이후 자식이 실행되면 세마포어의 값을 0으로 올려주고 부모가 실행할 수 있는 조건이 만족된다.
1.4 생산지/소비자(유판 버퍼) 문제
세마포어로 생산자/소비자를 구현하기 위해선 세가지 기법이 필요하다.
full, empty 조건 추가(두개의 세마포어 값을 사용한다.)
상호 배제 추가하기
교착 상태 방지
full, empty 조건 추가
생산자는 소비자의 세마포어에만 영향을 주어야하고, 반대로 소비자는 생산자의 세마포어에만 영향을 주어야한다.
그렇기 때문에 생산자는 empty를 wait하고 full을 post한다.
소비자는 full을 wait하고 empty를 post한다.
생산자는 empty 락을 획득해야지 값들을 버퍼에 추가할 수 있으며, 버퍼가 다차면 소비자에게 락을 전달해 버퍼에 값을 소비하라고 요청해야한다.
소비자는 full 락을 획득해야지 버퍼에 있는 값들을 소비할 수 있으며, 버퍼가 비게 되면 생산자에게 락을 전달해 버퍼에 값을 넣어달라고 요청해야한다.
상호 배제의 추가
두 개의 생산자 pa, pb가 동시에 put()을 호출하게 되면 pa가 버퍼에 값을 넣고 fill 카운터 변수를 1로 증가시키기 전에 인터럽트가 발생하고 pb가 이어서 같은 버퍼 공간에 값을 덮어 쓰게 된다.
버퍼를 채우고 버퍼에 대한 인덱스를 증가하는 해당 동작은 임계 영역이 된다.
아래의 코드와 같이 락을 put()과 get() 앞 뒤로 추가해준다.
하지만 위의 코드는 교착상태로 인해 코드가 동작하지 않는다.
교착 상태의 방지
생산자와 소비자 쓰레드 중 소비자가 먼저 실행되었다. mutex를 획득 후 full 변수에 대하여 sem_wait()을 호출한다. 버퍼가 비어있다. 소비자는 대기모드로 전환된다. CPU를 양보하게 되는데
여기서 중요한 것은 소비자가 아직 mutex 락을 보유하고 있기 때문에 생산자는 실행할 수 없다.
이를 해결하기 위해서 mutex의 순서를 아래와 같이 변경해준다.
1.5 Reader-Writer 락
다양한 자료구조를 접근할 때, 각 자료 구조의 특성과 접근 방식을 적절히 고려한 여러 종류의 락 기법이 필요하다.
다수의 쓰레드가 연결 리스트에 노드를 삽입하고 검색을 하는 상황을 가정해보자.
삽입 연산이 없다는 보장만 된다면, 다수의 검색 작업을 동시에 수행할 수 있다.(삽입은 리스트를 변경하기 때문에 임계영역이 된다.)
이와 같은 경우를 위해 만들어진 락이 reader-writer 락이다.
자료구조를 “갱신” 하려면 배타적 접근권한을 갖는 락을 사용한다.
읽기 락은 동시에 여러 쓰레드가 락을 보유할 수 있다.
읽기 락을 획득시 ㅈritelock 세마포어에 대해 sem_wait()을 호출하여 쓰기 락을 함께 획득한다.
획득한 쓰기 락은 읽기 락을 해제할 때 sem_post()로 다시 해제한다.
쓰기 락을 획득하려는 쓰기 쓰레드들은 모든 읽기 쓰레드가 끝날 때 까지 대기하여야 한다.
1.6 식사하는 철학자
철학자 p0~p4 가 양쪽에 있는 포크 f0~f4를 양 손으로 들고 내려두는 행동을 모든 철학자가 할 수 있도록 코드를 작성해야한다.
이 포크를 잡기 위한 경쟁과 그에 따른 동기화 문제가 병행 프로그래밍에서 다루려는 식사하는 철학자 문제이다.
불완전한 해답
위와 같은 코드로 순서대로 왼쪽 포크와 오른쪽 포크를 한 사람씩 든다고 가정해보자.
간단히 해결될 것 같아도, 1번 철학자가 f1을 든 상황에서 인터럽트가 발생하고, 2번 철학자가 f2를 들게 된다면, 1번 철학자는 f2를 들지 못해 락을 반납하지 못하고 교착 상태에 빠지게 된다.
해답: 의존성 제거
이 문제를 해결하기 위한 가장 간단한 방법은 최소한 하나의 철학자가 다른 순서로 포크를 집도록 하면 된다.
한번 그림을 그려보며 어떻게 동작하는지 확인해보자!
1.7 쓰레드 제어
문제: “과도하게 많은” 쓰레드가 동시에 수행되면 시스템의 효율이 매우 나빠진다.
정답: “과도하게 많은” 에 임계값을 정하고, 세마포어를 사용하여 문제가 되는 코드를 동시에 실행하는 쓰레드 개수를 제한한다.
우리는 이 접근법을 제어(throttling)라고 부르며 수락 제어의 한 형태로 간주한다.
댓글남기기