[OS] 동기화 기법
1. 동기화 종류
- 크게 3가지 종류가 있음
- 뮤텍스(mutex)
- 스핀락(spinlock)
- 세마포어(semaphore): 자원이 여러개인 경우
1.1. locks 방식
- 뮤텍스(mutex), 스핀락(spinlock)
- 상호배제가 되도록 만들어진 락(lock) 활용
- Mutex는 동기화 대상이 only 1개일 때 사용
- 락을 소유한 스레드만이 임계구역 진입
- 락을 소유하지 않은 스레드는 락이 풀릴 때까지 대기
- 둘의 차이는 busy-wait, sleep-wait (큐 존재)
(< 사진삽입필요 >)
1.2. wait-signal 방식
- 세마포어(semaphore)
- n개 자원을 사용하려는 m개 멀티스레드의 원활한 관리
- 카운터를 활용!
- 자원을 소유하지 못한 스레드는 대기(wait)
- 자원을 다 사용한 스레드는 알림(signal)
- (사실 얘도 Busy-wait, sleep-wait로 구분됨)
(< 사진삽입필요 >)
2. Mutex
- MUTual EXclusion의 줄임말
- 잠김/열림 중 한 상태를 가지는 락 변수 이용
- 한 스레드만 임계구역에 진입시킴, 다른 스레드는 큐에 대기
- sleep-waiting lock 기법
2.1. 구성 요소
- 락 변수
- true/false 중 한 값
- true : 락을 잠근다. 락을 소유한다.
- false : 락을 연다. 락을 해제한다.
- 대기 큐
- 락이 열리기를 기다리는 스레드 큐
- 연산
- lock 연산(임계구역의 entry 코드)
- 락이 잠김 상태(lock = true)이면, 현재 스레드를 Block 상태로 만들고 대기 큐에 삽입
- 락이 열린 상태이면, 락을 잠그고 임계구역 진입
- unlock 연산(임계구역의 exit 코드)
- lock = false, 락을 열린 상태로 변경
- 대기 큐에서 기다리는 스레드 하나 깨움
- lock 연산(임계구역의 entry 코드)
2.2. 그림
(< 사진삽입 필요 >)
2.3. 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
const int count = 200000;
int sum = 0; // global variable, shared data
pthread_mutex_t lock;
void* myThread1(void *p) {
for(int i = 0; i < count; i++) {
pthread_mutex_lock(&lock); // entry
sum += 1;
pthread_mutex_unlock(&lock); // exit
}
return 0;
}
void* myThread2(void *p) {
for(int i = 0; i < count; i++) {
pthread_mutex_lock(&lock); // entry
sum -= 1;
pthread_mutex_unlock(&lock); // exit
}
return 0;
}
int main() {
pthread_t tid1, tid2; // thread id
int count = 200000;
int *ret1, *ret2;
printf("Start!\n");
pthread_mutex_init(&lock, NULL); // init mutex
pthread_create(&tid1, NULL, myThread1, NULL);
pthread_create(&tid2, NULL, myThread2, NULL);
pthread_join(tid1, (void**)&ret1); // waiting for 'tid1'
pthread_join(tid2, (void**)&ret2); // waiting for 'tid2'
printf("sum = %d\n", sum);
pthread_mutex_destroy(&lock); // destroy mutex
return 0;
}
3. Spinlock
- 이전 포스팅의 while(lock)으로 대기하던 것과 같음
- 바쁜 대기(busy wait) 기법
- 뮤텍스의 non-blocking 모델 (Busy-waiting)
- 락이 잠겨 있을 때 블록되지 않고 락이 풀릴 때까지 검사 코드 실행
- 큐가 아닌 While loop로 대기!
- 락이 잠겨 있을 때 블록되지 않고 락이 풀릴 때까지 검사 코드 실행
- 단일 CPU(단일 코어)를 가진 운영체제에서 비효율적, 멀티 코어에 적합
- 단일 코어 CPU에서 의미 없는 CPU 시간 낭비
- 스핀락을 검사하는 스레드의 타임 슬라이스가 끝날 때까지 다른 스레드 실행 안 됨, 다른 스레드의 실행 기회 뺏음
- 여러 스레드/프로세스가 동시 접근을 시도하는 경우 기아 상태에 빠질 수 있음
- 락을 소유한 다른 스레드가 실행되어야 락이 풀림.
- 임계구역의 실행 시간이 짧은 경우 효과적
3.1. 그림
(< 사진삽입필요 >)
3.2. 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
const int count = 200000;
int sum = 0; // global variable, shared data
pthread_spinlock_t lock; //스핀락 변수 생성
void* myThread1(void *p) {
for(int i = 0; i < count; i++) {
pthread_spin_lock(&lock); // 임계구역 entry 코드, 스핀락 잠그기
sum += 1;
pthread_spin_unlock(&lock); // 임계구역 exit 코드, 스핀락 열기
}
return 0;
}
void* myThread2(void *p) {
for(int i = 0; i < count; i++) {
pthread_spin_lock(&lock); // entry
sum -= 1;
pthread_spin_unlock(&lock); // exit
}
return 0;
}
int main() {
pthread_t tid1, tid2; // thread id
int count = 200000;
int *ret1, *ret2;
printf("Start!\n");
pthread_spin_init(&lock, NULL); // init spin(스핀락 변수 초기화)
pthread_create(&tid1, NULL, myThread1, NULL);
pthread_create(&tid2, NULL, myThread2, NULL);
pthread_join(tid1, (void**)&ret1); // waiting for 'tid1'
pthread_join(tid2, (void**)&ret2); // waiting for 'tid2'
printf("sum = %d\n", sum);
pthread_spin_destroy(&lock); // destroy spin
return 0;
}
4. Mutex vs Spinlock
4.1. 락이 잠기는 시간이 긴 경우 : 뮤텍스
- 락을 얻지 못했을 때, CPU를 다른 스레드에게 양보하는 것이 효율적
- 락이 잠기는 시간이 짧은 경우 : 스핀락이 효율적
4.2. 단일 CPU를 가진 시스템 : 뮤텍스
- 단일 CPU에서 스핀락은 크게 의미 없음
4.3. 멀티 코어(멀티 CPU)를 가진 시스템 : 스핀락
- 잠자고 깨는 컨텍스트 스위칭 없이 바로 자원 사용
- 임계구역은 가능한 짧게 작성하므로
4.4. 사용자 응용프로그램 : 뮤텍스, 커널 코드 : 스핀락
- 커널 코드나 인터럽트 서비스 루틴을 빨리 실행되어야 하고, 인터럽트 서비스 루틴 내에서 잠잘 수 없기 때문
4.5. 스핀락을 사용하면 기아 발생 가능
- 스핀락은 무한 경쟁 방식이어서 기아가 발생 가능
- 락을 소유한 스레드가 락을 풀지 않고 계속 실행하거나 종료해버린 경우, 코딩이 잘못된 경우 시
5. 세마포어(semaphore)
- semaphore 뜻: 수기(手旗) 신호, 수기 신호로 알리다
- n개의 공유 자원을 다수 스레드가 공유하여 사용하도록 돕는 자원 관리 기법
- 보다 다수의 자원을 다수의 프로세스/쓰레드를 대상으로…
- e.g., n개의 프린터가 있는 경우, 프린터를 사용하고자 하는 다수 스레드의 프린터 관리
- 보다 다수의 자원을 다수의 프로세스/쓰레드를 대상으로…
- 세마포어는 동시에 여러 개의 프로세스/스레드가 임계 구역에 접근할 수 있도록 카운터를 가지고 있는 입구!
- → 일정 수가 초과하면 못들어갑니다!
5.1. 구성 요소 (counter semaphore)
- 자원 : n 개
- 대기 큐 : 자원을 할당받지 못한 스레드들이 대기하는 큐
- counter 변수
- 사용 가능한 자원의 개수를 나타내는 정수형 전역 변수
- n으로 초기화(counter = n)
- P/V 연산
- P 연산 (wait 연산): 자원 요청 시 실행하는 연산 (자원 사용 허가를 얻는 과정)
- V 연산 (signal 연산): 자원 반환 시 실행하는 연산 (자원 사용이 끝났음을 알리는 과정)
5.2. P 연산, V 연산
- P/V를 wait/signal로 표기하기도 함
- P 연산 : 자원 사용을 허가하는 과정, 사용가능 자원 수 1 감소(counter–)
- V 연산 : 사용 사용을 마치는 과정, 사용가능 자원 수 1 증가(counter++)
- 세마포 종류 2가지 - 자원을 할당받지 못한 경우의 행동에 따라 구분
1
2
3
4
5
6
7
8
9
// busy-wait 세마포어
P(S) {
while S <=0; // 아무것도 하지 않음 (반복문)
S--;
}
V(S) {
S++;
}
1
2
3
4
5
6
7
8
9
10
11
12
// sleep-wait 세마포어
P(S) {
S--;
if S < 0
// 이 프로세스를 재움 큐에 추가 (잠 듦)
}
V(S) {
S++;
if S <= 0
// 재움 큐로부터 프로세스를 제거 (깨어남)
}
5.3. 세마포어 통한 자원 관리 예제
- 4개의 인스턴스를 가진 자원에 대해, 4개의 스레드(T1~T4)가 할당 받아 사용, 2개의 스레드 T5와 T6는 자원을 기다리고 있는 상태
- counter 변수는 사용 가능한 자원의 개수를 나타냄
- 음수이면 대기 중인 스레드의 수를 나타냄
5.4. 이진 세마포어: 특수한 경우
- 이진 세마포(binary semaphore)
- 자원이 1개있는 경우 멀티스레드 사이의 자원 관리
- 1개의 자원에 대해 1개의 스레드만 액세스할 수 있도록 보호
- 뮤텍스와 매우 유사
5.4.1. 구성 요소
- 세마포 변수 S
- 0 과 1 중 하나를 가지는 전역 변수, S는 1로 초기화
- 대기 큐
- 사용 가능한 자원이 생길 때까지 스레드들이 대기하는 큐
- 스레드 스케줄링 알고리즘 필요
- 2개의 원자 연산
- wait 연산(P 연산) – 자원 사용 허가를 얻는 과정
- S가 1 감소 시키고, 0보다 작으면 대기 큐에서 잠듦, 0보다 크거나 같으면, 자원 사용하는 코드 실행 - signal 연산(V 연산) – 자원 사용이 끝났음을 알리는 과정
- S를 1증가시키고, 0보다 크면 그냥 리턴, 0보다 작거나 같으면
5.5. 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
#include <semaphore.h> //세마포어 사용
#include <unistd.h>
sem_t sem; //세마포어 구조체 생성
void* myThread1(void *p) {
int cnt = -1;
sem_wait(&sem); // P 연산, 자원 사용 요청
sem_getvalue(&sem, &cnt); // get the semaphore cnt
printf("%s uses the resource. Remaining %d \n", (char*)p, cnt);
sleep(1);
sem_post(&sem); // V 연산, 자원 사용 끝
sem_getvalue(&sem, &cnt); // get the semaphore cnt
printf("%s finished using the resource. Remaining %d \n", (char*)p, cnt);
}
int main() {
int cnt = -1;
pthread_t tid[5];
char *name[] = {"t1", "t2", "t3", "t4", "t5" };
printf("Start!\n");
sem_init(&sem, 0, 3); // set the semaphore count to 3
sem_getvalue(&sem, &cnt);
printf("1. Semaphore: %d \n", cnt);
for(int i = 0; i < 5 ; i++)
pthread_create(&tid[i], NULL, myThread1, (void*)name[i]);
for(int i = 0; i < 5 ; i++)
pthread_join(tid[i], NULL);
sem_getvalue(&sem, &cnt);
printf("2. Semaphore: %d \n", cnt);
sem_destroy(&sem);
return 0;
}
5.6. 세마포어의 오사용
- 프로세스가 세마포어를 사용하지 않고 바로 임계구역에 들어간 경우로 임계구역을 보호할 수 없음
- P를 두 번 사용하여 wake_up 신호가 발생하지 않은 경우로 프로세스 간의 동기화가 이루어지지 않아 세마포어 큐에서 대기하고 있는 프로세스들이 무한 대기에 빠짐
- P와 V를 반대로 사용하여 상호 배제가 보장되지 않은 경우로 임계구역을 보호할 수 없음
- (C/C++에서의 포인터 문제와 유사!)
5.7. 세마포어 vs 뮤텍스
- 가장 큰 차이점: 동기화 대상의 갯수
- Mutex는 동기화 대상이 only 1개일 때 사용
- Semaphore는 동기화 대상이 1개 이상일 때 사용
- 세마포어는 뮤텍스가 될 수 있지만, 뮤텍스는 세마포어가 될 수 없음
- Mutex는 0, 1로 이루어진 이진 상태를 가지므로 Binary Semaphore!
- Mutex는 자원 소유 가능 + 책임을 가지는 반면, Semaphore는 자원 소유 불가
- 뮤텍스는 상태 0, 1 뿐이므로 Lock 가질 수 있음
- Semaphore는 책임이 없음
- Mutex는 소유하고 있는 스레드만이 현재 Mutex를 해제할 수 있음
- 반면, Semaphore는 Semaphore를 소유하지 않는 스레드가 Semaphore를 해제할 수 있음
6. Monitor(모니터)
공유 자원을 내부적으로 숨기고 공유 자원에 접근하기 위한 인터페이스만 제공함으로써 자원을 보호하고 프로세스 간에 동기화를 시킴 (→ 추상화)
- 작동원리
- 임계구역으로 지정된 자원에 접근하고자 하는 프로세스는 직접 P나 V를 사용하지 않고 모니터에 작업 요청
- 모니터는 요청받은 작업을 모니터 큐에 저장한 후 순서대로 처리하고 그 결과만 해당 프로세스 에 알려줌
- 일종의 클래스 개념
- c.f., 스마트 포인터
- 자바로 작성한 모니터 내부 코드
1
2
3
4
5
6
7
8
9
10
11
12
monitor shared_balance {
private int balance = 10;
private boolean busy = false;
private condition mon;
public increase(int amount) {
if (busy == true) mon.wait();
busy = true;
balance += amount;
mon.signal();
}
}
이 포스팅은 작성자의 CC BY-NC 4.0 라이선스를 준수합니다.