28장: Locks
27장 복습: Lock API 함수
전역변수로 선언
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t init = PTHREAD_COND_INITIALIZER;
임계 영역에 상호 배제 기능 제공
- pthread_mutex_lock = 임계영역 lock 요청, lock을 할 수 없다면 대기
- pthread_mutex_trylock = lock 요청, lock 못 얻으면 대기X 리턴
- pthread_mutex_unlock = lock 해제, 대기중인 스레드는 lock 얻음
Lock 사용 법
Lock 변수 선언 및 초기화
pthread_mutex_init(뮤텍스 객체, 속성) - 성공하면 0 반환
임계영역 시작에서 Lock, 끝나면 Unlock
lock 사용 다 끝나면, 삭제
27장: Condition Variables (조건 변수)
쓰레드 간 특정 신호를 주고 받을 때 사용
pthread_cond_init
프로듀서-컨슈머 문제에서 임계영역 안에서 Queue가 비어있다고,
단순 wait()를 박으면, 다른 프로세스가 진입할 수도 있음
이 때, phtread_cond_wait를 사용함
pthread_cond_signal = 대기하는 스레드를 깨워줌
pthread_cond_broadcast = 대기하는 모든 스레드를 깨움
사용 방법
// A 스레드
pthread_mutex_lock(&lock);
while(ready == 0) // 레디 큐
pthread_cond_wait(&init, &lock);
pthread_mutex_unlock(&lock);
임계 영역에 들어왔는데 레디 큐가 비어있는 경우 사용
스레드를 Sleep 상태로 보내고 lock을 해제함
// B 스레드
pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&init); // init을 기다리는 스레드 하나 깨움
pthread_mutex_unlock(&lock);
만약 조건변수를 쓰지 않으면, spin lock이 걸려서 CPU 시간 낭비됨
단일 프로세스의 경우 다른 스레드가 작업 X
while(ready == 0) ;
Lock 개요
Lock 변수
- Lock의 상태를 저장함
- 사용 가능: 락 소유자 없는 상태
- 사용 중: 정확히 하나의 스레드가 락을 획득하여 임계영역에 진입한 상태
Granularity (사용범위)
Coarse-grained
적은 수의 락으로 넓은 범위 처리
Fine-grained
많은 수의 락으로 작은 임계영역 처리
Lock/Unlock 함수는 어떻게 만들까 ?
- 어떤 하드웨어 지원이 필요한가 ?
- 어떤 운영체제 지원이 필요한가 ?
- 만들어진 락은 무엇을 기준으로 평가할까 ?
락의 평가 기준
- Mutual Exclusion (상호배제)
- 한 번에 하나의 스레드만 임계 영역에 진입할 것이 보장되는지
- Fairness (공정성)
- 각 스레드가 락 획득을 위한 공정한 기회를 얻는지,
- 기아 상태 스레드가 없는지
- Performance (성능)
- 락을 사용하는데 드는 시간적 오버헤드를 최소화할 수 있는가
CISC 아키텍처
Lock 구현 (1): 인터럽트 제어
lock을 얻으면 인터럽트 기능 꺼버림
문제점
- 락 획득을 원하는 스레드에 특권 명령 허용
악의적으로 무한점거 해버릴 수 있음 - 멀티 프로세서에 적용 불가
- 락 획득을 원하는 스레드에 특권 명령 허용
Lock 구현 (2): SW 제어
### 장점
- SW만으로 가능
### 단점
- 이해하기 어려움
- 비효율적
Lock 구현 (3): TestAndSet (하드웨어 도입)
SW의 문제점
→ No Mutual Exclusion, Test와 Set이 분리되었기 때문
flag = 1로 설정 못 했는데 인터럽트 당하면 답이 없음
= Test와 Set이 분리되었기 때문
SW만으로 하지 않고, 하드웨어 활용
HW Atomic Operation을 추가하여 lock 구현
Test와 Set을 분리하지 않고 실행
!
int TestAndSet(int* old_ptr, int new) {
int old = *old_ptr;
*old_ptr = new; // set
return old; // test
}
이러면 중간에 인터럽트 당하지 않고, 테스트와 셋 동시 설정 가능
= 원자적으로 수행
typedef struct __lock_t {
int flag;
} lock_t;
void init(lock_t* lock) {
lock->flag = 0; // 0은 사용가능, 1은 사용중
}
void lock(lock_t* lock) {
while(TestAndSet(&lock->flag, 1) == 1);
}
void unlock(lock_t * lock) {
lock->flag = 0;
}
TestAndSet 함수에서 flag는 1로 세팅되고, 0을 리턴함
다른 프로세스가 들어오면 0으로 세팅, 1이 리턴 == true = busy waiting
TestAndSet 평가
- Mutual Exclusion = Yes
flag 0 값을 가지고 1로 변경하는 것은 하나의 스레드만 가능 - Fairness = No
누가 먼저 진입할지, 차례대로 기회 받을 수 있는지 모름 - Performance = ?
Single CPU: CPU 시간 낭비
Multiple CPUs: 꽤 좋음
Lock 구현 (4): CompareAndSwap
ptr이 가리키고 있는 주소값이 expected 변수와 일치하는가 ?
- 일치하면 ptr이 가리키는 주소 값을 새 값으로 변경
- 불일치하면 아무것도 안함
int CompareAndSwap(int* ptr, int expected, int new) {
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}
void lock(lock_t* lock) {
while(ComparedAndSwap(&lock->flag, 0, 1) == 1) ; // Spin
}
만약 현재 lock→flag가 1이라면,
두 번째 인자 0과 비교해서 다르면 flag를 바꾸지 않음
RISC 아키텍처
Lock 구현 (5): Load-Linked And Store-Conditional
Load-Linked = 메모리 적재
현재 lock→flag 불러옴
Store-Conditinal
Load-Linked 해서 flag가 0인걸 확인했어도, 그 사이에 flag가 바꼈을 수도 있음
if 문으로 한 번 더 검사하고, flag 값 바꿔줌
int LoadLinked(int* ptr) {
return *ptr;
}
int StoreConditional(int* ptr, int value) {
if (LoadLinked 명령 후, *ptr이 변경되지 않은 경우) {
*ptr = value;
return 1;
} else {
return 0;
}
}
void lock(lock_t* lock) {
while(1) {
while(LoadLinked(&lock->flag) == 1) ;
if (StoreConditional(&lock->flag, 1) == 1)
return;
}
}
void unlock(lock_t* lock) {
lock->flag = 0;
}
==
while(LoadLinked(&lock->flag) || !StoreConditional(&lock->flag, 1))
Lock 구현 (6): Fetch-and-Add
특정 주소의 값을 반환하면서 동시에 증가
int FetchAndAdd(int* ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}
typedef struct __lock_t {
int ticket;
int turn;
} lock_t;
void lock_init(lock_t* lock) {
int myturn = FetchAndAdd(&lock->ticket);
while(lock->turn != myturn) ;
}
void unlock(lock_t* lock) {
FetchAndAdd(&lock->turn);
}
공정성 만족
Too Much Spining
Spin lock = Busy Waiting
⇒ 변하지 않을 값을 기다리며 CPU 소모
임계 영역이 길 때 문제 발생
해결 방법
- 무조건 양보
- 큐의 사용: Sleep Lock
- OS마다의 해결책
- 2단계 락
1. 무조건 양보
void init() {
flag = 0;
}
void lock() {
while(TestAndSet(&flag, 1) == 1)
yield();
}
void unlock() {
flag = 0;
}
Spin 해야할 때 무조건 양보
yield = 레디큐 맨 끝
- 문맥 교환 비용이 과도해질 수 있음
스레드가 100개 이상이면, 계속 Context Switch - 특정 스레드의 기아를 막을 수 없음
→ 공정성이 무너짐
2. 큐사용: Sleep Lock
Sleep에 들어가면 대기 큐로 가게 함
락 사용이 가능할 때, 대기중인 스레드를 깨움
필요한 시스템 API
- park()
- 스스로 대기 상태로 전이
- unpark(threadID)
- threadID로 지정된 스레드를 깨움
TestAndSet으로 Sleep Lock 구현
- lock 자료구조 초기화
typedef struct __lock_t {
int flag; // 임계영역 진입용
int guard; // flag와 큐 동작 보호용
queue_t* q; // 락 대기자 전용 큐
} lock_t;
void lock_init(lock_t* m) {
m->flag = 0;
m->guard = 0;
queue_init(m->q);
}
- lock & unlcok 사용
- 처음에 flag와 guard가 0으로 세팅되어 있음
- A 스레드가 lock 진입
TestAndSet에서 Guard 검사로 1로 바뀜 + 스핀락 안 걸림
if (m→flag == 0)에 의해 flag를 1로 설정하고 guard를 0으로 세팅함 - 이제 B 스레드가 Lock에 진입
while문에 안 걸림 = 스핀락 발생 X,
만약 A가 guard=0 안 했으면 스핀락 ㅇㅇ
A가 flag를 1로 해둬서 else문으로 들어가서 본인을 queue에 넣음
guard=0으로 세팅함
이제 park()을 해야 하는데, 이때 A가 unlock 과정에서 Unpark() 해버림
4. A가 unlock 과정에서 Queue가 비어있지 않으니까
else 문으로 들어가서 unpark()을 수행해버림
5. B 스레드가 park()을 하면 더이상 unpark() 해줄 수 있는 애가 없게 됨
void lock(lock_t* m) {
while(TestAndSet(&m->guard, 1) == 1) ;
if (m->flag == 0) {
m->flag = 1;
m->guard = 0;
} else {
queue_add(m->q, gettid());
m->guard = 0;
park(); // 이 때, 경쟁조건이 발생할 수 있음
}
}
void unlock(lock_t* m) {
while(TestAndSet(&m->guard, 1) == 1) ;
if (queue_empty(m->q))
m->flag = 0; // Lock 대기자 없을 때만 flag 해제
else
unpark(queue_remove(m->q));
// flag 해제 X, 다음 스레드를 깨움
m->guard = 0;
}
Wake Up/Waiting Race 문제점
- A 스레드: park() 직전에 인터럽트 당함
- B 스레드: 락 사용 종료 → 큐에 대기자가 없으므로 락을 해제함
- A 스레드: 남은 park() 마저 수행 → 영원히 못 깨어남
해결
setpark() 사용
: 다른 스레드가 미리 unpark() 하면 park()은 수행 X
queue_add(m->q, gettid());
setpark(); // park()을 호출할 예정이라고 표시함
m->guard = 0; // 선취점 발생
park();
setpark()을 했는데, 다른 스레드가 인터럽트 했음
그러고 그 스레드가 lock 작업 마치고 Unpark()을 했다면,
다시 돌아온 기존 스레드는 park()을 수행하지 않음
'🖥️ Computer Science > Operating System' 카테고리의 다른 글
[OS] 31장: Semaphore (0) | 2024.07.04 |
---|---|
[OS] 30장: Condition Variables (0) | 2024.07.03 |
[OS] 19장: TLB (Translation Lookaside Buffers) (1) | 2024.07.01 |
[OS] 18장: 메모리 가상화의 Paging (0) | 2024.06.30 |
[OS] 17장: Free-Space Management (0) | 2024.06.29 |