6-1에서 동기화를 다루는 임계구역 문제를 알아봤고, 피터슨 알고리즘을 통해 소프트웨어 해결책의 한계를 알아봤다.
6-2에서는 이 한계를 해결하기 위한 하드웨어의 지원을 알아봤다.
하지만 6-2에서 언급한 TAS와 CAS는 응용 프로그래머가 직접 쓰기 힘들다.
이번 글에서 TAS와 CAS를 쓰기 쉽게 만든 소프트웨어 도구인 뮤텍스, 세마포어, 모니터를 다룬다.
이 글에서 설명하는 소프트웨어 도구들은 임계구역 문제를 해결하기 위한 3가지 요구사항 중 상호배제에 초점을 두고 있다.
즉 이 도구들을 적용시켜도 데드락, 기아 문제가 발생할 수 있다는 의미이다.
기아 문제는 CPU 스케줄링과 비슷한 아이디어를 통해 해결할 수 있을 것이다.
하지만 데드락의 경우 좀 복잡하다. 이 내용은 8장에서 다룰 것이다.
먼저 소프트웨어 도구들에 대해 살펴보기 전에 용어를 하나만 정리하자.
집을 들어갈 때 열쇠를 통해 들어가는 것처럼
락(Lock)은 임계구역에 들어가기 위한 키로 생각하면 된다.
뮤텍스는 상호배제를 만족하는 가장 간단한 소프트웨어 도구로
Mutual Exclusion(상호배제)를 축약한 단어이다.
공유 자원에 대한 프로세스(스레드)의 접근을 1개로 제한할 때 사용하기 좋다.
뮤텍스는 available
이라는 하나의 boolean
타입의 락을 가지고,
acquire()
를 통해 락을 획득하고 release()
를 통해 락을 반환하는 방식이다.
즉 하나의 락을 사용한다.
이 함수는 모두 CAS를 통해 구현할 수 있지만 쉬운 이해를 위해 간단한 코드로 풀어쓴 것이다.
그러니 acquire()
, release()
모두 동기화에 대한 문제없이 실행된다고 생각하자.
이제 뮤텍스를 활용해서 임계구역 문제를 해결하면 다음과 같다.
이 코드는 상호배제를 만족하여 동기화 문제를 해결한다.
뮤텍스 락의 acquire()
와 release()
를 다시 살펴보자.
acquire()
는 while
문을 통해 available
가 false
가 되길 기다리고 있다.
즉 acquire()
에서 반복문을 돌며 블로킹된다
여러 프로세스가 실행중 한 프로세스가 임계구역에 진입하여 코드를 실행중일 때 다른 프로세스들은 반복문을 반복하며 대기해야 한다.
이렇게 반복문을 돌며 락을 획득하길 기다리는 것을 바쁜대기(Busy Wait)이라고 부른다.
뮤텍스와 같이 바쁜대기(Busy Wait)를 기반으로 구현한 락 유형을 스핀락이라 부른다.
락을 사용할 때까지 프로세스가 회전하기 때문이다.
반복문을 계속 돌면서 락을 획득할 때까지 기다리는 방식은 비효율적인 것 같다고 생각할 수 있다.
반복문을 돌지 않고 락을 획득할 때까지 대기하려면 어떻게 해야할까?
프로세스를 대기 상태로 돌리고 락을 획득할 수 있을 때 실행해주면 될 것 같다.
이 아이디어는 세마포어에서 이어진다.
무조건 나쁘지는 않다.
멀티 코어 환경에서 특정 프로세스가 스핀락을 통해 대기하는 상황과
대기 상태로 전환된 상황을 비교해보자.
2개의 코어가 있는 상황에서
1번 코어에서 동작하는 P1이 락 획득했고,
2번 코어에서 동작하는 P2가 뒤늦게 락을 획득하려는 상황이다.
왼쪽은 P2가 락을 획득하기 위해 스핀락을 도는 예시이고
오른쪽은 P2가 스핀락을 피하기 위해 대기상태로 전환한 예시이다.
예시 1 : P2의 스핀락 | 예시 2 : P2의 대기전환 |
---|---|
락을 획득하지 못함 | 락을 획득하지 못함 |
반복문 돌림 | 대기 상태로 전환 컨텍스트 스위칭 |
P1이 락반환 | P1이 락반환 |
P2가 바로 락 획득 | P2가 레디 상태로 깨어남 |
임계구역 진입 | 실행 상태로 전환 컨텍스트 스위칭 |
빠르게 작업 시작 | P2가 락 획득 |
작업... | 임계구역 진입 |
작업... | 스핀락보다 느리게 작업을 시작 |
스핀락을 사용하지 않고 대기 상태로 전환되는 경우에
컨텍스트 스위칭으로 인해 빠르게 임계구역에 진입하지 못했다.
예시로 든 상황에서는 P1이 락을 빨리 반환했기 때문에 스핀락을 통해 반복문을 돌던 P2가 빠르게 임계구역에 진입할 수 있었다.
(스핀락은 많은 운영체제에서 실제 사용하고 있다.)
하지만 락을 오랜 시간 유지하는 경우 대기 상태로 전환하는 것이 더 좋다.
스핀락을 사용하는 경우
락이 유지되는 시간 < 컨텍스트 스위칭 2번 하는 시간
세마포어는 뮤텍스에서 발전된 형태이다.
공유 자원에 대한 프로세스(스레드)의 접근을 여러개로 제한할 때 사용하기 좋다.
세마포어는 다음과 같은 구조를 사용한다.
뮤텍스에서 사용하던 available
이 int
타입의 value
로 바뀌었고,
프로세스 list
가 추가됐다.
즉 true와 false만 가능했던 뮤텍스와 다르게 정수형 int
타입을 사용하여 여러 스레드가 임계구역에 진입할 수 있도록 만들었다.
Busy Wait 피하기
항목에서 언급된 아이디어를 다시 생각해보자.
효율적으로 대기하기 위해 프로세스를 대기상태로 돌린다고 했다.
이때 대기하는 프로세스의 정보를 담기위해 list
를 사용한다.
세마포어는 wait()
과 signal()
함수를 사용한다.
wait()
에서는 value--
를 시키고 만약 세마포어의 value
가 0보다 작다면 list
에 추가하고 프로세스를 대기 상태로 전환시킨다.
signal()
은 value++
을 시키고 만약 세마포어의 value
가 0보다 크다면 list
의 임의의 프로세스를 깨운다. (깨우고 싶은 임의의 프로세스를 선택하는 과정은 달라질 수 있다. FIFO 등등)
Mutex와 마찬가지로 임계구역 밖에 wait()
과 signal()
함수를 적절하게 사용하면 임계구역 문제를 해결할 수 있다.
뮤텍스와 유사하지만 세마포어는 value 값을 조절하여 1개 뿐만 아니라 여러개의 프로세스(스레드)가 임계구역에 진입하게 만들 수 있다.
뮤텍스와 같이 1개의 프로세스만 허용하는 세마포어를 이진 세마포어라 부르고,
여러개의 프로세스를 허용하는 세마포어는 카운팅 세마포어라 부른다.
다시 wait()
과 signal()
의 코드를 살펴보자.
wait()
의 코드와 signal()
내부의 코드는 결국 전부 원자적으로 실행되어야 한다.
결국 wait()
과 signal()
도 임계구역이라는 의미이다.
여러 프로세스가 세마포어의 value
를 동시에 ++
시키거나 동시에 list에 프로세스를 넣거나 제거하면 동기화 문제가 발생한다.
결국 다시 바쁜 대기(Busy Wait)를 해야 한다.
Busy Wait 피하기
항목에서 언급됐듯 뮤텍스의 Busy Wait를 피하기 위해 세마포어에서는 프로세스를 대기 상태로 전환했다.
그럼에도 불구하고 세마포어에서는 Busy Wait가 발생했다.
결국 Busy Wait을 피하지 못한것 아닌가?
아니다. 이 둘은 범위에서 차이가 난다.
뮤텍스는 락 획득을 시도할 때 임계영역에 들어간 프로세스가 락을 반환하길 기다렸다.
세마포어는 락 획득을 시도할 때 wait()
함수에 대한 락을 획득하고 대기상태로 들어간다.
wait()
, signal()
내부의 명령어는 길어도 10줄을 넘지 않기 때문에
바쁜 대기는 자주 발생하지 않는다.
이렇게 세마포어는 Busy Wait을 완전히 제거하지 못했지만, 범위를 wait()
, signal()
로 줄였다는 것을 알아야 한다.
앞서 2가지의 소프트웨어 도구로도 충분히 상호배제를 만족하며 동기화 문제를 해결할 수 있다.
하지만 여전히 사용하기 까다롭다.
항상 임계구역에 진입하기 전에는 wait()
을 호출해야하고 임계구역에서 나올 때 signal()
을 호출해야 한다는 규칙을 지켜야 한다.
signal()
을 먼저 호출한다면?siganl()
과 wait()
의 위치가 뒤바뀌였다면?프로그래머가 살짝만 잘못해도 에러가 발생한다.
추후 추가 예정
도서 - 운영체제
인프런 - 운영체제 공룡책 강의
https://dev.to/rinsama77/process-synchronization-with-busy-waiting-4gho