프로세스는 병렬로 실행될 수 있다는 점을 알았다. 이는 특정 프로세스는 어느 지점에서는 인터럽트 되고, 처리 코어는 다른 명령어를 실행하도록 할당될 수 있다.
이 과정에서 동기화 문제가 발생한다.
예를들어, count
라는 변수를 공유하는 두 프로세스 a
,b
가 있다고 하자.
a
는 count++
를 수행한다.b
는 count--
를 수행한다.각 명령어는 우리 입장에서는 하나의 명령이지만 처리하는 컴퓨터 입장에서는 그렇지 않다.
register1 = count
register1 = count +1
count = register1
결국 명령어를 수행하기 위해서는 메모리에서 레지스터로 데이터를 가져오는 과정이 필요하다. 또 연산 후 다시 메모리에 적재해야 한다.
만약 a
프로세스가 메모리에 계산으로 업데이트 된 값을 적재하기 전에 인터럽트가 발생하여 b
가 실행된다면, a
,b
가 각각 한 번씩 실행되어 count
값이 그대로 유지되어야 하는데 기대와는 다른 결과값이 나오게 된다.
이렇게 되려면 분명 타이밍이 맞아야하고 그럴 확률은 적다고 볼 수도 있다. 하지만 이러한 오류는 치명적이므로 동기화 문제를 해결할 방법들이 등장한다.
이렇게 여러 프로세스가 동일한 자료를 접근하고 그 실행 결과가 접근이 발생한 특정 순서에 의존하는 상황을 경쟁상황(race condition)이라고 한다.
race condition이 발생할 가능성이 있는 코드 영역을 critical-section이라고 부른다.
임계구역 문제에 대한 해결안은 세가지 요구 조건을 충족해야 한다.
상호 배제(mutual exclusion): 임계구역 내에서는 하나 이상의 프로세스가 동시에 실행될 수 없다.
진행(progress): 특정 임계구역 내에 실행되고 있는 프로세스가 없고, 해당 임계구역에 진입하고자 하는 프로세스들이 있다면, 나머지구역에서 실행중이지 않은 프로세스들만 다음에 누가 진입할지에 참여할 수 있으며, 이 선택은 무기한 연장될 수 없다.
한정된 대기(bounded waiting): 임계구역에 진입을 요청한 순간부터 진입하기까지 다른 프로세스들이 해당 임계구역에 진입하도록 허용되는 횟수에 한계가 있어야 한다.
단일 코어에서는 공유 변수를 수정하는 동안 인터럽트 발생을 막는다면 임계구역 문제를 간단히 해결할 수 있다.
하지만 다중 코어 환경에서는 간단히 해결이 불가능하다. 한 코어에서 인터럽트를 막는다고 해도 다른 코어들을 통제하기엔 비효율적이고 보장되지 않는다.
Peterson 해결안은 두 프로세스가 두 개의 데이터 항목을 공유하도록 하여 해결한다.
int turn;
boolean flag[2];
turn
은 임계구역으로 진입할 프로세스의 번호를 나타낸다.
flag[i]==true
는 i
번 프로세스가 임계구역에 진입할 준비가 되었다는 것을 의미한다.
즉 turn==i
이고 flag[i]==ture
라면 i
프로세스가 임계구역에 진입한다.
while (true) {
flag[i] = true;
turn = j;
while (flag[j] && turn ==j);
/* critical section */
flag[i] = false;
/* remainder section */
}
turn==i
이고 flag[i]==ture
를 만족해야 한다.turn
변수는 동시에 두개의 값을 가질 수 없다. 따라서 임계구역 내에는 하나의 프로세스만 진입할 수 있다는 것이 보장된다.프로세스 구조를 보면 진입 전 turn
변수를 다른 프로세스의 값으로 설정한다.
이 변수가 바뀌기 위해서는 임계구역에 들어간 다른 프로세스가 임계구역을 탈출한 후 재진입 전에 turn
변수를 변경할 때 뿐이다. 그리고 그때까지 while문을 통해 공회전한다.
이는 특정 프로세스가 한 번 진입했다면 그 다음엔 대기중이던 프로세스도 한 번은 임계구역에 들어갈 수 있다는 것을 의미한다.
최신 컴퓨터 아키텍처에서는 성능 향상을 위해 프로세서 및 컴파일러가 종속성이 없는 읽기 및 쓰기 작업을 재정렬 할 수 있다. 따라서 Peterson 해결방식이
정상적으로 적용되지 않을 가능성이 존재한다.
컴퓨터 아키텍처가 응용 프로그램에게 제공하는 메모리 접근 시 보장되는 사항을 결정한 방식을 메모리 모델이라고 한다.
강한 순서 모델을 사용하는 경우 메모리의 모든 변경 사항을 다른 모든 프로세서로 전파하는 명령어를 제공하여 실행 중인 스레드에 메모리 변경사항이 보이는 것을 보장한다.
이러한 명령어를 메모리 장벽(memory barriers) 또는 메모리 펜스(memory fences)라고 한다.
test_and_set()
: 한 워드의 내용을 검사 및 변경
compare_and_swap()
: 두 워드의 내용을 교환
일반적으로 compare_and_swap 명령어는 상호배제를 제공하기 위해 직접 사용되기 보다는 다른 임계구역 문제 해결 도구를 구축하기 위한 기본 구성요소로 사용된다.
compare_and_swap()
연산을 사용하여 구현된다.원자적 변수는 모든 상황에서 경쟁조건을 완벽히 해결하지 않는다.
예를 들어, 생산자 소비자 문제에서 count
값을 원자적 변수로 선언했을 때 count
값 자체는 원자적으로 갱신된다. 하지만 소비자가 여러명이라면 count
값이 증가 됐을 때 여러명의 소비자가 동시에 버퍼에 진입할 가능성이 있다.
하드웨어 기반 해결책은 응용 프로그래머가 사용할 수 없다. 따라서 임계구역 문제를 해결하기 위한 상위 수준 소프트웨어 도구가 등장한다. 그 중 가장 간단한 도구가 mutex 락이다.
acquire()
함수를 호출하여 락을 획득해야 한다.release()
함수를 호출하여 락을 반환한다.mutex락의 acquire()
함수 구조는 다음과 같다.
acquire() {
while (!available)
; /* busy wait */
available = false;
}
이렇게 while loop를 이용해 대기하는 방식을 바쁜 대기라고 한다.
바쁜대기는 대기 중에도 CPU자원을 소모하기 때문에 락 획득을 위해 대기하는 프로세스가 많거나 대기 시간이 길어지는 경우 자원의 낭비를 유발한다.
이러한 방식을 스핀락이라고도 부른다.
반대로 대기 시간이 짧은 경우 스핀락이 유리하다. 여기서 짧다는 것은 대기 시간이 스레드를 대기상태로 옮기고 락이 사용해질 때 다시 준비상태로 복원하는 문맥교환에 소요되는 시간보다 짧은 시간을 의미한다.
mutex보다 정교하게 동기화할 수 있는 도구이다.
세마포 S는 정수 변수로서, 초기화를 제외하고는 표준 원자적 연산 wait()
와 signal()
로만 접근할 수 있다.
wait(S) {
while(S<=0)
; /* busy wait */
S--;
}
signal(S) {
S++;
}
이 두가지 연산은 원자적으로 실행되어야 한다.
세마포 값이 1인 경우 mutex와 동일하게 작동한다.
가용 자원의 개수만큼 세마포를 초기화하면, 해당 자원의 개수만큼만 프로세스가 접근할 수 있게 된다.
세마포를 활용하여 프로세스간 실행 순서도 제어할 수 있다.
ex)
synch
라는 세마포를 0으로 초기화하고,
S1
signal(synch);
wait(synch);
S2
이런식으로 구성하면 반드시 S1이 실행된 후에 S2가 실행되는 것을 보장한다.
typedef struct {
int value;
struct process *list;
} semaphore;
위와 같이 특정 세마포에 대기중인 프로세스 list를 관리한다.
signal()
연산을 통해 대기중인 프로세스가 있는 경우 대기큐에서 준비큐로 옮기는 작업을 진행한다.
세마포와 mutex는 함수 호출 순서에 따라 작동이 달라진다. 잘못 사용하면 발견하기 어려운 오류를 야기한다. (특정 실행순서에서만 발생, 항상 발생하지도 않음)
이러한 프로그래머의 실수를 방지하기 위해 동기화 도구를 통합하여 고급 언어 구조물을 제공한다. 그 중 하나가 모니터(monitors)이다.
라이브니스는 프로세스가 실행 수명주기 동안 진행되는 것을 보장하기 위해 시스템이 충족해야하는 일련의 속성을 말한다.
라이브니스 실패로 이어질 수 있는 두가지 상황을 본다.
만약 우선순위가 낮은 프로세스가 자원을 선점하고 있을 때, 우선순위가 더 높은 프로세스가 대기큐에 들어가거나 자원을 이용가능할 때까지 대기하는 현상이다.
이렇게 됐을 때 그 사이에 존재하는 프로세스가 가장 높은 우선순위인 프로세스보다 먼저 CPU자원을 할당받게 되는 현상이 발생한다. (높은 우선순위 프로세스가 대기큐에서 자원 사용을 기다리기 때문)
우선순위 상속 프로토콜을 구현하여 해결한다. 우선순위 상속 프로토콜이란 더 높은 우선순위 프로세스가 필요로 하는 자원에 접근하는 프로세스들은 자원 사용이 끝날 때까지 더 높은 우선순위를 상속받는다.