1. 락
- 여러 개의 명령어들을 원자적으로 실행해보고 싶지만 단일 프로세서의 인터럽트로 인해서(또는 멀티 쓰레드를 여러 프로세서에 병행성하려고 해서) 그렇게 할 수가 없었다.
- 프로그래머들은 소스 코드의 임계 영역을 락(lock)으로 둘러서 그 임계 영역이 마치 하나의 원자 단위 명령어인 것처럼 실행되도록 한다.
1.1 락: 기본 개념
- 락은 일종의 변수다.
- 락을 사용하기 위해서는 락 변수를 먼저 선언해야 한다.
- 이 락 변수는 락의 상태를 나타낸다.
- 락은 둘중 하나의 상태를 갖는다.
- 사용 가능(available) 상태 (unlockedf 또는 free)
- 즉 어떤 쓰레드도 락을 소유하고 있지 않다.
- 사용 중(acquired) 상태
- 즉 임계 영역에서 정확히 하나의 쓰레드가 락을 획득한 상태이다.
- 사용 가능(available) 상태 (unlockedf 또는 free)
- lock() 루틴 호출을 통해 락 획득을 시도한다.
- 이렇게 락을 획득한 쓰레드를 소유자(owner)라고 부른다.
- 락을 소유한 쓰레드가 임계 영역에 존재하는 상태에서는 다른 쓰레드들이 임계 영역으로 진입할 수 없다.
- 락 소유자가 unlock()을 호출한다면 락은 이제 다시 사용 가능한 상태가 된다.
- 일반적으로 쓰레드는 프로그래머가 생성하고 운영체제가 제어한다.
- 락으로 코드를 감싸서 프로그래머는 크 코드 내에서는 하나의 쓰레드만 동작하도록 보장할 수 있다.
1.2 Pthread 락
- 쓰레드 간에 상호 배제(mutual exclusion) 기능을 제공하기 때문에 POSIX 라이브러리는 락을 mutex라고 부른다.
- 상호 배제는 한 쓰레드가 임계 영역 내에 있다면 이 쓰레드의 동작이 끝날 때까지 다른 쓰레드가 임계 영역에 들어 올 수 없도록 제한한다고해서 얻은 이름이다.
- 각 데이터와 자료 구조를 보호하는 데 있어서, 여러 락을 사용한다.
1.3 락의 평가
- 락 설계시, 락의 정상동작 여부 판단을 위한 평가기준을 정해야 한다.
- 상호 배제를 제대로 지원하는가이다.
- 공정성(fairness)
- 락을 전혀 얻지 못해 굶주리는(starve) 경우가 발생하는지를 판단해야한다.
- 성능(performance)이다.
이제부터 1.4절부터 락을 구현하는 여러가지 방법에 대해 알아볼 것이다. 스핀락부터 스핀을 사용하지 않는 락까지 알아본다.
1.4 인터럽트 제어
- 초창기 단일 프로세스 시스템에서는 상호 배제 지원을 위해 임계 영역 내에서는 인터럽트를 비활성화하는 방법을 사용했다.
- 이 방법은 잔점이 많다.
- 첫 번째 단점은, 요청을 하는 쓰레드가 인터럽트를 활성/비활성화하는 특권(privileged) 연산을 실행할 수 있도록 허가해야 한다.
- 이를 다른 목적으로 사용하지 않음을 신뢰할 수 있어야 한다.
- 두 번째 단점은, 멀티프로세서에서는 적용을 할 수가 없다.
- 세 번째 단점은, 장시간 동안 인터럽트를 중지시키는 것은 중요한 인터럽트의 시점을 놓칠 수 있다.
- 마지막은, 이 방법은 비효율적이다.
1.5 오직 load/store 명령어만 사용하기(실패한 시도)
- load와 store 명령어만으로 락을 구현한다.
- 간단한 플래그 변수를 사용하여 쓰레드가 락을 획득하였는지를 나타낸다.
- 실패원인: 적시에 인터럽트가 발생하면 두 쓰레드 모두 플래그를 1로 설정하는 경우가 생길 수 있어서 임계 영역에 두 쓰레드 다 진입할 수 있게 된다.
- 성능저하: spin-wait라는 방법을 사용하여 플래그의 값을 무한히 검사하는데, 이는 시간을 낭비한다.
1.6 Test-And-Set을 사용하여 작동하는 스핀 락 구현하기
- 하드웨어 기법 중 가장 기본은 test-and-set 명령어 또는 원자적 교체(atomic exchange)로 알려진 명령어이다.
- 락의 값을 검사(test)</storng>하고 새로운 값으로 설정(set)하는 동작을 원자적 연산으로 만듦으로써 오직 하나의 쓰레드만 락을 획득할 수 있도록 만들었다.
- 지금 설명한 방법이 스핀 락으로 불리는 이유를 이제 이해할 수 있다.
- 락을 획득할 때까지, CPU 사이클을 소모하면서 회전한다.
- 이 방식을 제대로 사용하려면 선점형 스케줄러(preemptive scheduler)를 사용해야 한다.
- 선점형이 아니면, 단일 CPU에서 스핀 락의 사용은 불가능하다. 왜냐하면 while 문을 회전하며 대기하는 쓰레드가 CPU를 영원히 독점하기 때문이다.
스핀 락 평가
- 정확성
- 제대로 동작한다.
- 공정성
- 단순한 스핀 락은 공정하지 않으며 쓰레드가 굶주리게 만들 수 있다. (while문을 회전하기 때문에)
- 성능
- 단일 CPU의 경우 성능 오버헤드는 상당히 클 수 있다. 쓰레드는 할당받은 기간 동안 CPU 사이클을 낭비하면서 락을 획득하기 위해 대기한다.
- 멀티 CPU의 경우 다른 프로세서에서 락을 획득하기 위해 while문을 회전하면서 대기하는 것은 그렇게 많은 사이클을 낭비하지 않기 때문에 효율적일 수 있다.
1.7 Compare-And-Swap
- Compare-And-Swap 기법은 기본 개념은 ptr이 가리키고 있는 주소의 값이 expected 변수와 일치하는지 검사하는 것이다.
- CompareAndSwap 명령어는 TestAndSet 명령어보다 더 강력하다.
- 대기없는 동기화(wait-free synchronization)와 같은 주제를 다룰 때 이 루틴이 갖느 능력을 알게 될 것이다.
1.8 Load-Linked 그리고 Store-Conditional
- load-linked와 store-conditional 명령어를 앞뒤로 사용하여 락이나 기타 병행 연산을 위한 자료 구조를 만들 수 있다.
1.9 Fetch-And-Add
- Fetch-And-Add 명령어로 원자적으로 특정 주소의 예전 값을 반환하면서 값을 증가시킨다.
- 위의 방법은 티켓락
- 이전까지의 접근 방법과 이번 해법의 중요한 차이 중 하나는 모든 쓰레드들이 각자의 순서에 따라 진행한다는 것이다.
1.10 요약: 과도한 스핀
- 두개의 쓰레드를 프로세서가 하나인 시스템에서 실행하면 레이턴시가 증가한다.
- N개의 쓰레드가 하나의 락을 획득하기 위해 경쟁하게 되면 상황은 더욱 심각해진다.
- N-1개의 쓰레드에 할당된 CPU 시간 동안, 비슷한 이유로 낭비하게된다.
1.11 간단한 접근법: 조건 없는 양보!
- 위의 방법들로 동작이 검증된 락과 락 획득의 공정성(티켓 락을 사용한 경우) 까지도 해결할 수 있었다.
- 이전 쓰레드가 인터럽트에 걸리기 전에 락을 이미 획득한 상태라서 그 쓰레드가 락을 해제하기를 기다리며 스핀만 무한히 하는 경우에 어떻게 해야 할것인가?
- 첫 번째 방법은 락이 해제되기를 기다리며 스핀해야하는 경우 자신에게 할당된 CPU를 다른 쓰레드에게 양보하는 것이다.
- 위의 방법은 Test-And-Set와 양보를 이용한 락이다.
- 운영체제에 자신이 할당받은 CPU 시간을 포기하고 다른 쓰레드가 실행될 수 있도록 하는 yield90 기법을 사용한다.
- 단점은 많은 쓰레드가 있을 때 하나를 제외한 나머지의 쓰레드가 실행과 양보를 반복하는 패턴으로 비용이 많이 든다. 또한 어떤 쓰레드는 무한히 양보만 하고 있는 경우가 있을 수 있다.
1.12 뮤의 사용: 스핀 대시 잠자기
- 이전 방법들의 근본 문제는 너무 많은 부분을 운에 맡긴다는 것이다.
- 다수의 쓰레드가 락을 대기하고 있을 경우, 다음으로 락을 획득할 쓰레드를 명시적으로 선택할 수 있어야 한다. 이를 위해서는 운영체제의 적절한 지원과 큐를 이요한 대기 쓰레드들의 관리가 필요하다.
- park(), unpark() <- 쓰레드를 잠재우고 깨우는 함수이다.
- 장점
- 앞서 배운 Test-And-Set 개념을 락 대가지 전용 큐와 함께 사용하여 좀 더 효율적인 락을 만들 수 있다.
- 큐를 사용하여 다음으로 락을 획득할 대상을 제어하여 기아 현상을 피할 수 있도록 할 수 있다.
1.13 2단계 락
- 첫 번째 단계에서는 회전하며 대기한다. 락이 빠른 시간 내에 해제될 것을 가정한다.
- 만약 첫 단계에서 락을 획득하지 못했다면 두 번째 단계로 진입한다.
- 두 번째 단계에서 호출자는 차단된다.
- 락 해제시 블럭된 쓰레드중 하나를 잠에서 깨운다.
댓글남기기