개발계발
동기화 본문
동기화란?
프로세스(스레드)들 사이의 자원의 일관성을 보장하고 수행 시기를 맞추는 것으로 크게 2가지로 나뉜다.
- 실행 순서 제어 : 올바른 순서대로 실행되도록
- 상호 배제 : 동시에 접근해서는 안되는 자원에 하나의 프로세스(스레드)만 접근하게 하기
실행 순서제어를 위한 동기화
어떤 값을 쓰고자하는 프로세스와 읽고자 하는 프로세스가 실행 중일 때, 이 두 프로세스의 실행 순서는 쓰고나서 읽어야 한다. 이렇게 프로세스를 올바른 순서로 실행되게 하는 것이 실행 순서제어 동기화이다.
(이 동기화는 별 거 아닌 것 같지만 중요한 것은 상호 배제이다.)
상호 배제를 위한 동기화
공유 자원의 동시 사용으로 인해 결과값이 달라지는 상황을 피하기 위해 사용
상호 배제를 하지 않았을 때 발생하는 대표적인 문제로 생산자와 소비자 문제가 있다.
생산자와 소비자 문제 예시 코드
public class ThreadExample {
private static int sum = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("초기 합계: " + sum);
Thread producer = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
sum++;
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
sum--;
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("producer, consumer 스레드 실행 이후 합계: " + sum);
}
}
위 코드는 처음에 0으로 선언된 변수에 10,000 더하는 스레드와 10,000 빼는 스레드를 동시에 실행하게 하는 함수이다.
위 코드의 실행 결과를 예측해보면 당연히 10,000을 더하고 10,000을 뺐으니까 다시 0이 나와야 되는거 아닌가?
라고 생각할 수 있지만 실제 실행결과는 아래와 같다.
심지어, 실행할 때마다 다른 결과가 나온다.
-> 동시에 접근하면 안 되는 자원에 스레드가 동시에 접근했기 때문인데, 이렇게 잘못된 실행으로 여러 프로세스가 동시에 임계 구역의 코드를 실행해 문제가 발생하는 경우(결과가 달라지는 경우)를 레이스 컨디션이라고 한다
그럼 이런 문제가 왜 발생할까??
-> sum++, sum--와 같은 고급언어의 코드는 원자적이지 않기 때문!
'총합++'라는 코드를 저급언어로 변환하면 다음과 같은 실행순서를 거친다.
r1 = 총합 // 총합 변수를 레지스터에 저장
r1 = r1 + 1 // 레지스터 값 1 증가
총합 = r1 // 레지스터 값을 총합 변수에 저장
위 3줄의 코드가 실행되는 동안 아래와 같이 문맥 교환이 발생할 수 있다.
공유자원과 임계 구역
위의 예시 코드에서 전역 변수인 sum을 공유자원이라고 한다. 공유 자원은 위처럼 전역 변수가 될 수도 있고, 파일, 입출력장치, 보조기억장치가 될 수도 있다.
동시에 실행하면 문제가 발생하는 자원(위 예시에서는 sum변수)에 접근하는 코드 영역을 임계구역이라고 한다.
-> 두 개 이상의 프로세스가 임계 구역에 진입하고자 하면 둘 중 하나는 대기해야 함.
즉, 상호배제를 위한 동기화란 두 개 이상의 프로세스가 임계 구역에 동시에 접근하지 못하도록 관리하는 것이다.
동기화 기법
임계 구역에 하나의 프로세스만 진입하게 하고, 올바른 신행 순서를 보장하기 위한 방법으로 뮤텍스 락, 세마포, 모니터가 있다.
뮤텍스 락
프로세스가 임계구역에 진입 후 자물쇠(락)를 걸어 스스로 락을 해제하기 전까지 다른 프로세스가 진입하지 못하게 하는 기법
뮤텍스 락은 전역변수 하나(lock)과 두개의 함수(acquire, relase)로 아래와 같이 구현할 수 있다.
acquire() {
while (lock == true);
lock = true;
}
release() {
lock = false;
}
위 두개 함수를 임계 구역 전후로 호출해 하나의 프로세스만 임계 구역에 진입할 수 있도록 한다.
acquire();
// '총합' 변수 접근 (임계구역)
release();
하지만, 위 방식은 바쁜 대기(busy waiting)방식을 이용하고 있어 개선이 필요하다.
->while(lock == true); 의 코드를 보면 lock이 true일 경우 false로 바뀔때까지 무한히 대기(계속해서 확인)하는 방식
세마포
뮤텍스 락과 비슷하지만, 공유 자원이 여러 개 있는 상황에서도 적용이 가능한 동기화 기법이다.
프로세스가 임계구역에 진입하기 전 스위치를 '사용 중'으로 놓고, 마치면 사용이 완료 됐다는 신호를 다음 프로세스에게 보내는 방식이다
-> 바쁜 대기를 할 필요 없음.
세마포는 임계 구역에 진입할 수 있는 프로세스의 개수(전역 변수 S)와 두 개의 함수(wait, signal)함수로 구성된다.
(일반적으로 wait은 P, signal은 V로 표현하기도 함)
S = 접근 가능한 총 프로세스 수;
wait () {
S--;
if ( S <0 ) {
이 프로세스를 대기 큐에 삽입;
sleep(); // 대기 상태로 진입
}
}
signal () {
S++;
if ( S <= 0 ) {
이 프로세스를 대기 큐에서 제거;
wakeup(p); // 대기 상태의 프로세스 p를 준비상태로 변경
}
}
-> 뮤텍스 락과 차이점
: 바쁜 대기를 이용하지 않는다, 스레드가 잠금을 소유하지 않는다(소유권 개념이 없음), 여러 스레드가 접근할 수 있음
모니터
위에서 세마포어까지 살펴봤는데, 코드가 방대해지거나 세마포를 잘못된 사용했을 경우(세마포를 누락한 경우, wait(), signal()함수 순서를 잘못 기재한 경우 / 중복해서 사용한 경우) 문제가 발생할 수 있다.
공유 자원을 사용할 때 모든 프로세스가 세마포어 알고리즘을 따른다면 P(), V()함수를 사용할 필요 없이 자동으로 처리하면 된다.
이를 구현한 것이 모니터 기법이다.
위 그림을 보면 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근할 수 있다. 이를 단계별로 구분해 보면
1. 임계구역에 접근하고자 하는 프로세스는 직접 P(), V()함수를 사용하지않고 모니터에 작업 요청을 한다.
2. 모니터는 요청받은 작업을 모니터 큐에 저장해놨다가 프로세스가 요청한 공유자원 연산을 처리하고 그 결과만 프로세스에 알려준다.