Lock
락은 여러 스레드 간에 자원을 접근하는 매커니즘을 제공한다. 일반적으로 상호 배제 정책을 통해, 하나의 스레드가 특정 자원에 접근중인 경우에는 다른 스레드가 접근하지 못하도록 제한한다. 락이 없다면 두 개 이상의 스레드가 동시에 자원에 접근할 수 있으므로, 데이터의 무결성이 보장되지 않는다. 락의 개념은 멀티스레드를 사용하는 환경이라면 어디든 사용될 수 있다. ex)데이터베이스
락이 필요한 예시 - 여러 스레드가 동시에 공유 자원에 접근하는 경우
이해를 돕기 위해 간단한 예시를 들어보자. 스레드 A, B가 다음의 작업을 동시에 수행한다고 해보자. A, B는 변수 x를 공유한다.
스레드 A: x를 1 증가시킨다
스레드 B: x를 1 증가시킨다
그런데 'x를 1 증가시킨다'라는 작업은 기계어 상으로 다음과 같이 나눠진다.
1. x를 읽는다 - (메모리로부터 x를 읽어 레지스터에 저장한다.)
2. x를 1 증가시킨다 - (레지스터를 1 증가시킨다)
3. 증가된 값을 x에 저장한다 (레지스터의 값을 x에 저장한다.)
그럼 x = 1이라고 해보자. 그럼 A와 B가 x를 동시에 읽고, 동시에 증가시키고, 동시에 쓴다면? x는 2가 된다. 분명히 연산은 두 번 했는데, 증가는 한 번만 된 것이다.
비슷하게 은행 송금을 예로 들 수 있다. 송금을 x에서 y로 한다면 다음의 순서일 것이다.
1. x의 돈이 충분한지 확인한다.
2. x의 돈을 송금한 금액만큼 뺀다.
3. y의 돈을 송금한 금액만큼 더한다.
x의 계좌에는 1000원이 있고 그걸 y에게 1000원을 송금한다고 할때 다음과 같은 상황이 벌어질 수 있다.
스레드 A | 스레드 B
x의 돈을 확인(1000원) | x의 돈을 확인 (1000원)
x의 돈을 0원으로 설정(-1000) |
y의 돈을 1000원 더함 | x의 돈을 0원으로 설정 (-1000)
| y의 돈을 1000더함
결과적으로 x의 계좌에서 1000원이 빠져나가고, y의 계좌에는 2000원이 들어와서 돈이 복사되었다.
Critical Section과 Atomic Operation
그렇다면 위의 예제에서 스레드 A와 B간의 동기화 문제를 해결하려면 어떻게 해야할까? 그건 송금의 연산 (돈 확인 - 돈을 뺀다 - 돈을 더한다)을 하나의 연산으로 묶어주면 된다. 이러한 연산을 원자적 연산 (Atomic Operation)이라고 한다. 원자적 연산이란 연산을 실행하는 도중에 다른 연산이 간섭하지 않는다는 의미이다. 아까의 상황에서는 스레드 A가 돈을 확인하는 도중에 스레드 B가 중간에 끼어들어서 원자적인 연산이 아니었지만, 돈 확인 - 돈을 뺀다 - 돈을 더한다라는 세 개의 연산이 끝나기 전까지, 다른 스레드가 끼어들지 못하게 한다면 이 문제를 해결할 수 있다.
여기서 나오는 개념이 바로 Critical Section이다. Critical Section이란 쉽게 말해 일종의 보호 구역으로서, Critical Section 안에서만 공유 자원 (예시에서는 x와 y의 돈)에 접근하도록 하고, 동시에 하나의 스레드만 Critical Section에 들어올 수 있게 한다면 위에서 말한 원자성을 보장할 수 있다. 위의 예시에서는 돈 확인 - 돈을 뺀다 - 돈을 더한다라는 일련의 작업들을 하나의 Critical Section으로 지정해줄 수 있다.
Lock과 Critical Section
자, 그래서 Critical Section과 원자성이 락과 무슨 관계인가? 쉽게 설명하자면 락을 통해서 Critical Section의 범위를 지정해줄 수 있다. 락은 열쇠라고 생각할 수 있다. 락을 잠가(획득, acquire)버려서 다른 스레드가 접근하지 못하도록 한다면, 그게 Critical Section이 시작되는거고 락을 풀어서(release) 다른 스레드가 Critical Section에 접근 가능하도록 하면 그게 바로 Critical Section이 끝나는 것이다.
---------- Critical Section 시작 ----------
lock
1. x의 돈이 충분한지 확인한다.
2. x의 돈을 송금한 금액만큼 뺀다.
3. y의 돈을 송금한 금액만큼 더한다.
unlock
----------- Critical Section 끝 -----------
자, 여기까지가 락의 개념이다. 이제 여기서 파생된 개념들의 주제를 던지고 이 글을 마무리하겠다.
Lock Overhead
락은 공짜가 아니다. 락을 생성하고, 소멸하는 것부터 락을 획득하고 반환하는 등 락을 사용하면 오버헤드가 생기게 된다. 그래서 락을 사용할때는 항상 어느정도의 비용이 발생하게 된다. 락을 많이 사용할수록 락 오버헤드가 많아진다. 실제 연산보다 락 자체를 사용하는 데 드는 비용이 커지는 것이다.
Lock Contention, Starvation
Contention은 투쟁, 논쟁 등을 의미하는데, lock을 획득하고자 하는 스레드가 2개 이상인 경우, 서로 경쟁하게 되며 경쟁으로 인해 락을 더 획득한 스레드와 덜 획득한 스레드가 생기게 된다. 심한 경우 다른 스레드들이 10번 락을 획득하는 동안 경쟁에 밀려서 1번도 락을 획득하지 못하는 기아 상태 (Starvation)의 스레드가 생길 수 있다. 이런 상황은 바람직하지 않으며, 모든 스레드가 공정하게 같은 비율로 락을 획득하는 것이 가장 이상적이다.
Granularity
Granularity(세분성)는 락이 얼마나 락이 얼마나 광범위한지, 또는 세밀한지를 의미한다. 락이 광범위해서, 락의 개수가 적을수록 lock/unlock을 하면서 발생하는 오버헤드는 줄어들겠지만 광범위한 만큼 스레드간의 경쟁이 중요해진다. 반대로 락이 매우 작은 범위를 감싸고 락의 개수가 많을수록 오버헤드는 증가하지만 경쟁은 완화된다. 상황에 맞춰서 락의 적절한 범위를 결정해야 한다.
Deadlock
데드락은 스레드들이 서로 락을 대기하느라 프로그램이 멈추는, 교착 상태에 빠지는 것을 말한다. 유명한 문제로 식사하는 철학자 문제가 있다. 원형 탁자에 N명의 철학자들이 앉아있고, N개의 포크가 철학자의 왼쪽과 오른쪽에 존재한다. 스파게티를 먹으려면 2개의 포크 (자신의 왼쪽, 오른쪽)가 필요한데, 그렇다면 왼쪽 포크 대기 - 왼쪽 포크 획득 - 오른쪽 포크 대기 - 오른쪽 포크 획득과 같이 포크 2개를 획득하는 과정이 있을 것이다. 그런데 여기서 모든 철학자가 동시에 왼쪽 포크를 획득한다면, 영원히 오른쪽 포크를 대기하면서 교착 상태에 빠지게 된다. 락을 사용할 때는 이러한 교착 상태를 초기에 방지해야 한다.
참고 링크, 출처
이미지 출처
'Kernel > Locking Primitives' 카테고리의 다른 글
[Linux Kernel] RCU (Read-Copy-Update) (1) | 2021.11.05 |
---|---|
[Linux Kernel] semaphore (2) - semaphore 분석 (0) | 2021.04.23 |
[Linux Kernel] semaphore (1) - semaphore의 개념과 사용법 (1) | 2021.04.22 |
[kernel/locking] spinlock (2) - Test And Set, Ticket, ABQL, MCS (0) | 2021.03.28 |
[kernel/locking] spinlock (1) - spinlock 사용법 (5) | 2021.03.27 |
댓글