저번 글에서 Thread에 대해 이야기 하면서 여러 Issue들을 다루었다.
race condition, critical section, indeterminate, mutual exclusion이 있었다.
4가지 개념이 무엇인지 생각하면서 보면 더 이해하기 쉬울 것 같다.
Local variable의 경우 스택에 저장된다. 각 스레드는 자신의 스택 공간을 가지기 때문에 스레드간에 공유되지 않는다.
Global variables은 static data 세그먼트에 저장되며, 어느 스레드도 접근이 가능하다. (앞의 글에서 thread간에 address space를 공유한다고 하였다.)
동적인 객체 또한 스레드간에 공유된다. heap에 저장되므로 마찬가지 이유로 스레드간에 공유 될 수 있다.
이제 local variable은 신경쓰지 않고, 공유되는 자원만 생각하자.
공유 되는 자원에 대한 Concurrency가 non-deterministic한 결과를 이끈다.
즉, 두 개 이상의 동시에 공유된 자원에 접근하는 스레드들은 race condition을 만든다.
따라서 공유 자원 접근에 대해 동기화(Synchronization) 메카니즘이 필요하다. (스케줄링은 프로그래머의 제어에 있지 않기 때문에 동기화 메카니즘이 필요)
동기화는 concurrency를 제한하여 수행된다.
critical section은 공유 자원에 대한 접근을 하는 코드의 부분을 말한다.
critical section에 대해서는 mutual exclusion이 필요하다. mutual exclusion은 다음 조건을 만족한다.
1. critical section을 원자적으로 수행한다. (모두 수행하거나 아예 하지 않거나)
2. 한번에 오직 하나의 스레드가 critical section에서 실행한다.
3. 모든 다른 스레드들은 진입을 기다려야 한다.
4. 한 스레드가 critical section을 나갈때, 다른 스레드가 들어갈 수 있다.
어떤 critical section은 마치 하나의 원자적인 instruction처럼 수행되는 것을 보장해야 한다.
Lock 변수는 lock의 상태를 가진다.
available(or unlocked or free) => lock을 쥔 스레드가 없다
acquired(or locked or hold) => 정확히 하나의 스레드가 lock을 가지고 있고, critical section에 진입해있을 것이다.
lock()
1. lock을 얻으려고 시도 한다.
2. 만약 다른 스레드가 lock을 가지고 있지 않으면, 해당 스레드는 lock을 얻는다.
3. critical section에 진입한다.
4. 해당 스레드가 lock을 가지고 있는 한 다른 스레드들은 이 critical section에 진입하는 것이 제한된다.
unlock()
1. 일단 lock의 주인이 unlock()을 호출하면, lock은 다시 이용가능해진다.
2. lock()에서 기다리고 있는 스레드를 깨운다.
lock의 사용을 정리하면
1. Lock은 초기에 free 상태이다
2. critical section에 진입하기전 lock()을 호출하고, 빠져나올 때 unlock()을 호출한다.
3. lock()을 호출한 caller가 lock을 쥘 때까지 return 하지 않는다.
4. unlock()까지, 스레드는 spin(spinlock)하거나 block(mutex)할 수 있다. (뒤에서 다룸)
5. 최소 한번에 한 스레드는 lock을 잡을 수 있다.
위 그림은 T1이 lock을 걸고 critical section을 수행하는 중에 T2가 lock을 시도하는 상황이다. T2는 T1이 unlock()을 호출 할 때까지 반복적으로 lock 변수를 확인한다.
Correctness
Fairness
Performance
Locks의 구현 방법에는 크게 3가지로 나눌 수 있다.
첫 번째 방법으로 critical section에 진입하면 interrupt를 disable 시키는 방법이다.
특징
문제점
위 코드는 thread 2개에 대한 spinlock을 구현한 것이다. 위 코드는 flag[self] = 1 이후에 context switch가 일어날 경우 Dead Lock이 발생한다.
이에 대한 해결책으로 Peterson's Algorithm이 있다.
추가적으로 turn 변수를 사용하여, 두 프로세스간에 critical section(mutual exclusion) 문제를 해결한다.
software-only algorithms은 다음과 같은 문제가 있다.
첫 번째로 아래 그림과 같이 mutual exclusion이 보장되지 않는다.
두 번째로는 lock함수 내부 코드를 보면 flag = 1이면 spin-wait을 한다. 이 때 CPU는 아무것도 안하고 소모되게 되므로 비효율적이다. 즉, 다른 스레드가 critical section을 빠져나오길 기다리면서 시간을 낭비하게 된다.
따라서 우리는 Hardware에 의해 지원되는 atomic instruction이 필요하다. (mutual exclusion 문제 해결)
Test And Set은 Hardware에서 MOVE나 ADD Instruction처럼 원자적인 instruction과 같이 지원한다.
성공시 1을 반환하고 ptr을 value로 값을 update한다.
실패시 ptr은 update 되지않고, 0을 반환한다.
특정 주소에 저장된 old value를 리턴하는 동안 해당 값을 계속 증가시킨다.
Ticket Lock
Ticket Lock은 fetch-and-add로 구성된다.
Correctrness: yes
spin lock은 오직 하나의 스레드가 critical section에 진입하도록 한다. (mutual exclusion)
Fairness: no
spin lock은 fairness를 보장하지 않는다. 즉, spinning 하는 thread는 영원히 spin할 수 있다.
Performance:
single CPU에서, performance overhead가 매우 좋지 않다. => timer interrupt까지 대기해야 한다. 예를 들어, T0가 context switch 직전에 lock을 걸고 끝난다면 다음 critical section을 수행하는 스레드 T1은 time slice만큼 loop를 돌며 기다리게 된다.
만약 CPUs의 수가 threads의 수와 대략적으로 비슷하다면, spin locks은 꽤 합리적으로 동작한다고 한다.
여기까지 Hardware-based spin locks을 살펴보았다.
Hardware-based spin locks은 간단하고 잘 작동하지만, 몇 케이스에서 이러한 solution은 꽤나 비효율적이다. => thread가 spinning하는 시간동안, 전체 time slice를 아무것도 하지 않고 값을 checking하는데 소비한다.
다음 포스팅에서는 OS의 도움을 받아 Spinning을 피하는 방법에 대해 알아보겠습니다.