저번 글에서는 spinlock을 이용하여 lock을 구현하는 방법을 알아보았습니다. 특히 Hardware-based spin lock들은 간단하고 잘 작동하였습니다.
이러한 해결책은 thread가 spinning을 할 때 time slice동안 값을 확인하는 것외에 아무 작업도 하지 않기 때문에 매우 비효율적일 수 있습니다.
따라서 OS의 도움을 받아 spinning을 피할 수 있는 방법에 대해 공부해보겠습니다.
=> OS의 Support
가장 간단한 방법으로 Just Yield가 있다. 말 그대로 현재 스레드가 spin을 하고 있으면, CPU를 다른 스레드에게 주는 방법이다
두 번째 방법으로는 lock에 진입을 기다리는 스레드를 추적할 Queue를 두는 것이다.
위 경우 Wakeup/waiting race가 발생할 수 있다.
Linux에서는 Solaris의 park와 unpark과 비슷한 futex를 제공한다.
v의 최상위 비트는 lock을 사용중인지 아닌지 확인한다.
나머지 모든 비트는 기다리고 있는 스레드 수를 나타낸다.
위의 mutex_lock함수 내부에서는 4가지 경우로 lock을 얻을 수 있다.
먼저 mutex_lock함수를 리턴한다는 것은 mutex_lock을 호출한 코드에서 리턴을 기다리며 wait하고 있는 상태가 풀리는 것이므로 lock을 얻는다고 이해하면 된다.
atomic_bit_test_set를 이용하여 31번 비트(최상위 비트)를 atomic하게 test와 set한다. 만약 리턴값이 0이면 mutext_lock 함수를 return하고 즉시 lock을 얻게 된다. 그렇지 않다면 atomic_increment를 수행하여 기다리고 있는 스레드 수를 하나 증가시키고 while문 내부로 들어간다.
while문 내부에서 다시 최상위 비트에 대해 test_set을 수행하고 lock을 얻을 수 있으면 atomic_decrement를 호출하여 기다리고 있는 스레드 수를 감소시키고 lock을 얻는다. 그렇지 않으면 v에 mutex의 값을 저장한다.
만약 v가 음이 아닌 정수이면 즉, 최상위 비트가 0인지 1인지 판단하여 양수이면 lock을 가진 스레드가 unlock을 했다는 의미이므로, continue문에 의해 while 반복문의 처음으로 돌아가서 test_set을 수행한다. 그렇지 않으면 futex_wait을 호출한다.
futex_wait에서도 v값과 mutex의 값이 다르면(lock을 가진 스레드가 unlock시 최상위비트 1이 0으로 바뀜) 즉시 리턴하여 lock을 얻게 된다.
mutext_unlock에서는 atomic_add_zero를 호출하는데 최상위 비트에 1을 더해주고, 0인지 확인한다. 만약 0이면 lock을 기다리는 스레드가 없다는 뜻이므로 리턴한다. lock을 기다리는 스레드가 존재할 경우 futex_wake를 호출하여 sleep큐에서 스레드 하나를 깨운다.
하지만 lock이 곧 풀릴 것인데, sleep을 호출하여 context switch를 하는 것은 손해일 것이다.
따라서 어느정도 spinning은 유용할 수 있다. 이 접근을 이용한 것이 Two-Phase Locks이다.
이렇게 OS의 도움을 받아 spin lock을 지양하여 구현하는 방법과 두 방법을 적절히 합친 방법까지 모두 알아보았습니다.
다음에는 Thread들간의 동작을 동기화 하기 위한 Condition Variable에 대해 알아보겠습니다.