-
락: 상호배제를 위한 API
이걸 어떻게 구현할 것이냐?.. 실제로는 하드웨어와 OS의 협업
1. 인터럽트로
2. 소프트웨어만 only approach
3. 소프트웨어가 하드웨어와 협업
락의 평가 방법
1. 정확성 : 상호 배제가 보장되는가
2. 공정성 : 기아 상태에 빠지는 스레드가 없는가
3. 성능 : 락, 언락에 의한 오버헤드나 낭비가 없는가
락 사이즈 이슈. 여러 공유 자원이 있을 때
Coarse-grained lock : 하나의 락으로 보호
- 장점: 심플
- 단점: 병행성이 별로
Fine-grained lock : 각각을 보호하는 락
- 장점: 병행성 굿
- 단점: 복잡
실제 락의 구현
1) Disable Interrupt
void lock() { DisableInterrupts(); } void unlock() { EnableInterrupts(); }
문맥 교환이 발생하지 않으면 문제가 안됨
근데 스케줄링은 타이머 인터럽트에 의해 발생 -> 인터럽트를 끄면 해결
장점: 단순
단점:
- 오랫동안 디스에이블하면 문제 발생. 타이머 인터럽트가 너무 오래 꺼지면 주기적인 작업들이 안될 수 있음
- 시스템 오용, 남용 가능. 독점 같은
- 코어 자체가 병렬적인 경우 -> 멀티플 프로세스가 되면 이걸로 해결이 안됨
* 1코어에서 오용되지 않는 경우 OS 내부에서만 사용하는 경우가 남아있음
2) Test-and-Set, 소프트웨어적인 방법
typedef struct __lock_t ( int flag; ) lock_t; void init(lock_t *mutex) { mutex->flag = 0; } void lock(lock_t *mutex) { while( mutex->flag == 1 ) ; mutex->flag = 1; } void unlock(lock_t *mutex) { mutex->flag = 0; }
flag를 두고 1이면 락 중이라 파악하고 스핀(비지 웨이팅), 0이면 락
문제: 애초에 안정확함
스레드 1, 2가 있다고 할 때 1번에서 flag 1로 바꾸기 직전에 2번 스레드로 넘어가면 락이 안됨
=> 락 API가 원자성이 보장이 안됨 => 상호 배제가 보장이 안됨
스핀이라 성능도 별로.. 웨이팅하면서 CPU를 안놔줌
굉장히 많은 시도가 있었고, 일부는 성공한게 있긴 함
장점: 순수 소프트웨어 솔루션이라 하드웨어 도움이 필요 없음
단점:
1. 이해하기 쉽지 않음
2. 하드웨어의 지원을 받는 것과 비교하면 성능이 별로
3. 성공한 것도 특수한 경우고, 최근에 만들어진 멀티코어 시스템, 제네럴한 시스템에서도 정확하지 않음
3) 하드웨어의 도움: atomoc operation
- a) Test-and-Set instruction (atomic exchange)
int TestAndSet(int *old_ptr, int new_num) { int old_num = *old_ptr; *old_ptr = new_num; return old_num; }
이 명령은 CPU 내부에서 원자적으로 실행이 됨
typedef struct __lock_t { int flag; } lock_t; void init(lock_t *lock) { lock->flag = 0; } void lock(lock_t *lock) { while( TestAndSet(&lock, 1) == 1 ) ; } void unlock(lock_t) { lock->flag = 0; }
while 문을 돌면서 락이 안풀렸으면 대기, 풀리면 다시 락하고 종료
1. 정확한가?
- 상호보장 됨
- 한 크리티컬 섹션에 하나의 스레드만 들어감
2. 공정한가
- 아니다
- 우선순위가 낮은 스레드는 먼저 들어와서 기다려도 계속 밀릴 수 있다 -> 티켓 락 방식이 있음
3. 성능
- 싱글 CPU : 스핀락이라 슬립 락보다 CPU 낭비가 있음
- 멀티 CPU : 락이 곧 해제된다고 하면 별로 나쁘진 않음
- b) Compare and Swap Instruction
int CompareAndSwap(int *ptr, int expected, int new) { int actual = *ptr; if( actual == expected ) *ptr = new; return actual; }
예상 값과 실제 값이 동일하면 새로운 값으로 교체
테스트 앤 셋과 거의 비슷한데 테스트 앤 셋은 일단 바꾸고 이거는 조건문 처리
void lock(lock_t *lock) { while( CompareAndSwap(&lock->flag, 0, 1) == 1 ) ; }
원래 값이 1이면 락이었다는 거니 스핀
- c) Load-Linked and Store-Conditional
MIPS, ARM같은 RISC 아키텍처에서 지원
int LoadLinked(int *ptr) { return *ptr; } int StoreConditional(int *ptr, int value) { if( no one has updated *ptr since LoadLinked to this address ) { *ptr = value; return 1; } else { return 0; } }
포인터에 다른 스레드가 접근한 적이 있으면 락하지 못하도록 하는 방법
void lock(lock_t *lock) { while(1) { while( LoadLinked(&lock->flag) == 1 ) ; if( StoreConditional(&lock->flag) == 1 ) return; } } void unlock(lock_t *lock) { lock->flag = 0; }
이중반복문,
일단 로드링크드로 락이 걸려있는지 확인하고,
스토어컨디셔널로 플래그가 건드려진 적 없으면 락 (원자성)
건드려졌으면 다시 바깥 while 루프를 타게 될 것
- d) Fetch-and-Add = 티켓 락
int FetchAndAdd(int *ptr) { int old = *ptr; *ptr = old + 1; return old; }
원자적으로 1 더하는 연산
typedef struct __lock_t { int ticket; int turn; } lock_t; void lock_init(lock_t *lock) { lock->ticket = 0; lock->turn = 0; } void lock(lock_t *lock) { int myturn = FetchAndAdd(&lock->ticket); while(lock->turn != myturn) ; } void unlock(lock_t *lock) { lock->turn = lock->turn + 1; }
락에 티켓과 락 변수를 둠
락: 티켓을 증가시키고 이전 값을 리턴하여 myturn에 저장
=> 부여받은 myturn이 현재 lock 진행중인 turn과 다르면 스핀
언락: 턴을 1 증가 시킴 => myturn이 해당하는 작업이 실행될 것
=> 번호표 시스템, 티켓 락이라고도 함
장점: 먼저 기다리던 잡이 턴이 작기에 나중에 온 애들보다 먼저 실행되는 것이 보장
기존의 테스트 앤 셋, 컴페어 앤 스왑, 로드 링크드 앤 스토어 컨디셔널 => 순서랄 게 없음 => 기아 상태(starvation)의 가능성
=> 프로그레스 보장, 페어니스 보장
락 메커니즘
Spin lock
- 비지 웨이팅
- 플래그가 바뀌었는지 계속 체크 (while)
- 심플
- CPU 낭비
Sleep lock
- 선점됨(preemptive). CPU를 반납하고 웨이팅
- 락을 할 때 이미 락이 사용 중이면 CPU를 반납하고 웨이트 큐에서 대기
- 문맥 교환을 해야하고 웨이트 큐 관리를 해야해서 더 복잡함
- CPU를 더 효율적으로 사용 가능
Sleep Lock의 구현
락이 반납되는 평균 대기 시간을 줄일 수 있음
문제는 두개
1. 어디서 슬립할 거냐 => 웨이트 큐
2. 언제 일어나냐 => OS 서포트
예) Sloaris의 park(), unpark()
typedef struct __lock_t { int flag; int guard; queue_t *q; } lock_t; void lock_init(lock_t *m) { m->flag = 0; m->guard = 0; queue_init(m->q); } 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; } else { unpark(queue_remove(m->q)); m->guard = 0; } }
* park, unpark는 솔라리스에서 쓰는 스레드 관련 API
guard를 추가로 사용 => 플래그를 위한 락 변수
가드에 테스트 앤 셋 => 일단 guard는 스핀을 해야 함
플래그가 0이면 수정 가능, 1이면 웨이트 큐에 넣고 슬립, 누군가 언락하면 깨어남
언락도 가드에 대해 테스트 앤 셋
큐가 비었으면 플래그를 0으로 바꿔주고,
안비었으면 큐에서 기다리던 잡을 꺼내기
슬립 락은 OS와 언어의 협업이 필요
'운영체제' 카테고리의 다른 글
조건 변수, 생산자/소비자 문제 (0) 2024.04.18 Lock-based Concurrent Data Structure (0) 2024.04.18 멀티 스레드 / 경쟁 상태, 공유자원, 임계영역, 상호배제 (0) 2024.04.10 Cache Affinity / job의 실행 시간 예측 (0) 2024.04.08 Proportional Share : 로터리 스케줄링, 스트라이드 스케줄링 (0) 2024.04.08