임계영역(Critical Section)은 멀티 프로세스(쓰레드) 환경에서 공유 자원을 변경하는 코드부분을 의미한다. 두 개 이상의 프로세스(쓰레드)가 동시에 임계영역에 진입하게 되면 데이터 불일치와 같은 문제상황이 발생할 수 있다.
발생하는 문제상황은 Race Condition, Deadlock, Starvation이 존재한다.
임계영역에서 발생하는 문제를 해결하기 위해 임계영역에는 여러 프로세스(쓰레드)가 동시에 접근하지 않도록 보호해야한다. 이를 위해 Lock이라는 개념을 적용한다.
Lock을 구현하는 방법은 언어마다 다르고 행동하는 방법에 따라 여러 방식이 존재한다. 기본적으로는 한 쓰레드가 임계영역에 전급하려할 때 다른 쓰레드에 의해 Lock된 상황이라면 쓰레드의 상태는 Block이나 Wait으로 변한다. 임계영역에 있는 쓰레드는 작업을 다 끝내면 임계구역을 Unlock한다.
Lock을 구현하는 방법은 다음과 같다.
Lock은 상호배제를 구현하기 위한 방법! Lock을 얻은 쓰레드는 임계영역에 자신만 사용할 수 있다는 확신을 가질 수 있다.
만약 어떤 쓰레드가 임계구역의 Lock을 얻지 못했다면 Lock을 얻기위해서 쓰레드는 어떤 행동을 취해야 할까?
각각의 방법은 장단점이 존재하는데
3번을 구현할 때는 디자인 패턴의 옵저버 패턴을 생각해보자!
여러 자원에 Lock을 할 때 순서가 중요하다. Lock순서가 서로 다른 쓰레드를 동시에 실행하다도면 교착상태가 발생할 수도 있다.
예를 들면, Thread1
은 A->B->C 순서로 Lock을 얻고, Thread2
는 C->B->A 순서로 Lock을 얻는 상황이 있다 하자.
Thread1
가 A에 대한 Lock을 얻고 B에 대한 Lock을 얻는다.Thread2
가 C에 대한 Lock을 얻는다.Thread1
는 Thread2
가 가지고 있는 C에 대한 Lock을 얻기 위해 대기한다.Thread2
는 Thread1
이 가지고 있는 B에 대한 Lock을 얻기 위해 대기한다.위 상황은 교착상태가 발생한 상황이고 만약 Thead1
과 Thread2
가 같은 잠금순서를 가지고 있다면 Thread2
는 처음에 A에 접근하게 될 것이고 Thread1
은 C에 대한 Lock을 얻을 수 있다.
Lock의 잠금순서는 교착상태를 예방하기 위한 방법중 하나이다.
SpinLock은 자원에 대한 Lock이 풀릴 때까지 루프를 돌며 계속 대기하는 Lock방법으로, Lock된 자원에 접근하고자 하는 쓰레드는 Block당하면 공유자원에 대한 접근을 계속 시도한다.
SpinLock의 단점은 Lock을 얻을 수 있는 시간이 보장되지 않을 수 있다는 것이다. 어떤 자원이 한번 Lock되고 오랜시간동안 풀리지 않는 자원이라면 해당 자원에 대한 Lock을 얻기 위해 다른 쓰레드들은 매우 오랜시간동안 대기해야 할 것이다.
만약, Block되었을 때 일정시간 뒤에 다시 시도하도록 구현하려면 어떻게 해야할까? SpinLock은 Block 당했을 때 바로 자원의 Lock을 얻는것을 시도한다. 그렇다면, Block후 Loop안에 있는 쓰레드는 일정시간 동안 대기상태에 있을 필요가 있다.
일정시간 동안
Wait
상태에 있는 것은Thread.Sleep
이나Thread.Yield
를 사용할 수 있다.
ReadWriteLock은 읽기(Read)작업과 쓰기(Write)작업을 따로 제공하는 Lock방법으로 특정한 자원에 대한 읽기 작업은 동시에 이뤄질 수 있지만 쓰기작업은 하나의 쓰레드에서만 제공하는 방법이다.
ReadWriteLock의 단점은 읽기 작업이 쓰기 작업보다 많이 발생하는 경우에 효율적이라는 것이다. 만약, 쓰기 작업이 더 잦다면 쓰기에 대한 Lock을 얻기 위해 다른 쓰레드는 오래 기다리게 될 것이고 이는 곧 연산자원의 낭비가 된다.