💋 임계 영역(Critical Section)
지난 포스팅에서 언급한 것처럼, 여러 프로세스/스레드가 동시에 실행되더라도 공유 데이터의 일관성을 유지하기 위해서는 동기화가 필요합니다.
동기화에 대한 문제를 해결하기 위해서, 개발자들은 하나의 메서드에 한 번에 딱 하나의 스레드만이 진입해서 실행(= mutual exclusion
)할 수 있도록 하는 방법을 생각해 냈습니다.
이때, 이 메서드의 영역을 임계 영역
(Critical Section)이라고 부릅니다.
do {
// enter section -> 임계 영역에 진입할 요건이 되는지 확인
// critical section
// exit section -> 이후에 다른 스레드도 임계 영역에 진입할 수 있도록 처리
} while (true)
임계 영역 내에서는 Lock
을 사용해서 mutual exclusion
을 보장할 수 있습니다.
do {
// acquire lock -> 여러 스레드가 락을 획득하기 위해 경합
// critical section -> 락을 획득한 스레드만이 critical section에 들어가 실행
// release lock -> 락을 반환
} while (true)
매우 추상화된 설명이지만, 위와 같은 방식으로 섹션에 들어갈 때 락을 취득한 스레드만이 진입하고, 나갈 때 락을 반환하도록 하는 식으로 임계 영역을 구현할 수 있습니다.
💋 Critical Section Problem
임계 영역을 만족시키기 위해서는 아래에 나온 3가지 문제가 될 소지들을 해결해야 합니다.
아래 세 가지 문제들을 Critical Section Problem
이라고 부릅니다.
- 상호배제(Mutual Exclusion)
- 한 순간에 오직 하나의 스레드만이 임계 영역에 진입할 수 있음을 보장합니다.
- 여러 스레드가 동시에 임계 영역에 접근하지 못하도록 하는 것을 의미합니다.
- 진행(Progress)
- 어떤 스레드가 임계 영역에 진입하기 위해 대기 중인 상태에서, 다른 스레드가 임계 영역을 사용하고 있지 않다면, 다른 스레드가 진입할 수 있음을 보장해야 합니다.
- 제한된 대기(Bounded Waiting)
- 한 스레드가 임계 영역에 진입하기 위해 대기하는 시간은 무제한이 아니고, 제한되어 있어야 합니다.
- 즉, 어떤 스레드가 계속해서 다른 스레드에게 우선권을 주며 무제한으로 대기하는 상황이 발생하지 않도록 합니다.
💋 동기화 메커니즘
✔️ 개념
동기화 메커니즘은 여러 개의 스레드가 공유 자원에 동시에 접근하는 것을 조절하고 조율하는 데 사용되는 기술로, 주로 운영체제의 커널
(Kernel)에서 사용됩니다.
동기화 메커니즘이 있어야만, 공유자원에 대한 안전한 접근을 보장해 예측 가능하고 일관성 있는 프로그램을 만들 수 있습니다.
✔️ 종류
동기화 메커니즘으로 사용되는 도구로는 스핀락(Spinlock), 뮤텍스(Mutex), 세마포어(Semaphore) 등이 있습니다.
이들은 다중 스레드 환경에서 공유 자원에 대한 접근을 조절하여 임계 영역(critical section)에서의 경쟁 조건(race condition)과 같은 문제를 해결하기 위해 사용됩니다.
이제 그러면 도구인 스핀락, 뮤텍스, 세마포어를 차례대로 알아봅시다!
💋 스핀락(Spinlock)
✔️ 개념
Thread 0이 스핀락을 얻고 Critical Section에 진입해 공유 자원에 접근하게 되면, 스핀락을 얻지 못한 Thread 1은 대기하는 동안 락을 얻을 수 있는지 확인할 수 있는 작업을 무한반복합니다.
무한반복 하다가, 어느샌가 Thread 0이 critical section을 나와 스핀락을 반환하면, Thread 1은 스핀락을 얻고 critical section으로 들어갈 수 있습니다.
- 장점:
- 단순하고 구현이 쉽습니다.
- 락을 기다리는 동안 스레드가 대기하지 않고 계속 실행되므로, 일부 상황에서 빠를 수 있습니다.
- 단점:
- Busy-wait 방식으로 대기하면서 락을 얻을 수 있는지 확인하는 작업만으로도 계속해서 CPU 자원을 낭비하므로 효율성이 떨어질 수 있습니다.
- 운영체제가 제공하는 스케줄링 정책에 영향을 받을 수 있습니다.
✔️ 코드
(간이코드입니다. 실제로는 더 복잡합니다.)
global
한 변수인 lock
에 여러 스레드가 접근할 수 있습니다.
스레드들은 critical()
메서드에서 while
루프를 통해서 락을 획득하려고 시도하고, 그중 락을 획득한 단 하나의 스레드만이 critical section에 진입하게 됩니다.
의문이 하나 생깁니다.
✔️ testAndSet
메서드는 어떻게 mutual exclusion을 보장하나?
testAndSet
메서드는 코드만으로 구현한 것이 아니라, 이 메서드는 조금 특별하게 CPU의 도움을 받아서 한 번에 하나의 스레드만이 진입할 수 있게 구현되었습니다. 여러 개의 스레드가 testAndSet
메서드에 진입하려고 경합하더라도, 결과적으로 락을 취득하는 스레드는 반드시 1개가 됩니다.
⇒ testAndSet
메서드는 CPU atomic 명령어임.
atomic
(=단일 기계 명령어로 실행) 명령어 ⇒ 실행 중간에 간섭/중단 X- 같은 메모리 영역에서 동시에 실행 X
⇒ testAndSet
메서드는 동기화 보장!
- 다른 스레드가 현재 값을 읽어가기 전에 새로운 값을 먼저 설정 ⇒ Race Condition 방지
무튼 스핀락 방식은 CPU 사이클을 다른 스레드가 더 유용하게 사용할 수 있는데, 락을 기다리는 스레드가 락을 확인하는 데만 계속 사용하게 되어 CPU를 계속해서 낭비하기 때문에 비효율적입니다.
(락을 취득하지 못한 스레드는 락을 얻을 수 있을 때까지 락이 있어? 락이 있어? 락이 있어? 락이 있어? 락이 있어? … 락이 있어? 락 있네! critical section 진입〰️을 CPU에서 반복합니다.)
💋 뮤텍스(Mutex)
✔️ 개념
Mutual Exclusion의 약자로, 동시성 문제의 가장 간단한 해결방법 중 하나입니다.
하나의 스레드가 락을 얻어 critical section에 진입했다면, 락을 취득하지 못한 다른 스레드는 락을 가질 수 있을 때까지 휴식합니다.
✔️ 코드
(간이코드입니다. 실제로는 더 복잡합니다.)
코드를 통해서 간단히 개념에 대해서 이해해 봅시다!
value
를 취득해야만 critical section에서 동작할 수 있습니다. value
값을 통해서 critical section에서 단 하나의 스레드만이 실행될 수 있도록 합니다. guard
를 사용해서 value
값을 안전하게 변경할 수 있도록 하는 장치입니다.
lock()
에서 value
값을 안전하게 변경하기 위해서 guard
가 보장하는 critical section 내에서 value
값을 변경합니다. 마찬가지로, unlock()
메서드를 보더라도, value 값을 안전하게 변경하기 위해서 guard
가 보장하는 critical section 내에서 작업합니다.
결과적으로, 락을 얻고, 반환하는 과정에서 몇 가지 과정이 추가되었고, 이 과정마저 mutual exclusion을 보장해야 하기 때문에 guard
라고 하는 장치를 추가했다고 볼 수 있습니다.
락을 얻는 과정에서, 락을 얻지 못하고 대기해야 하는 스레드라면 큐에 넣고 잠자러 가는 과정이 추가되었고, 락을 반환하는 과정에서 잠자고 있는 스레드가 있다면 락을 반환하기 전에 깨우는 과정이 추가되었습니다.
✔️ Mutex VS Spinlock
뮤텍스는 대부분의 상황에서 스핀락보다 효율적으로 동작할 수 있으며, 특히 대기 시간이 길거나 대기 스레드가 많은 상황에서 더 효율적입니다.
스핀락은 언제 더 좋을까요?
멀티 코어 환경이고, critical section에서의 작업이 컨텍스트 스위칭보다 더 빨리 일어난다면, 스핀락이 더 효율적입니다.
컨텍스트 스위칭 ⇒ 스레드가 잠들고 일어나는 과정
멀티 코어 ⇒ 락을 얻은 후에 컨텍스트 스위칭이 필요 없는 환경
💋 세마포어(Semaphore)
✔️ 개념
signal mechanism
을 가진, 하나 이상의 스레드가 critical section에 접근 가능하도록 하는 장치입니다.
이제까지 살펴본 스핀락, 뮤텍스는 critical section에 진입할 수 있는 스레드의 개수가 1개로 고정되어 있었는데, 세마포어는 동시에 진입한 스레드의 개수를 1 이상으로도 설정할 수 있다는 점에서 다릅니다.
물론 세마포어에서도 value 값을 1로 설정하면, mutual exclusion을 구현할 수 있습니다.
value = 1
인 세마포어를 Binary Semaphore
(2진 세마포어)라고 하고, value > 1
인 세마포어는 Counting Semaphore
라고 합니다.
위 표에서 이 부분에 주목해보세요!
Spinlock
⇒ Only one processSemaphore
⇒ multiple processes
✔️ 코드
(간이코드입니다. 실제로는 더 복잡합니다.)
앞서 살펴본 뮤텍스 코드와 거의 동일한데, value가 1 이상이 될 수 있다는 점이 다릅니다. 뮤텍스에서는 value에 해당하는 값을 0 또는 1로만 관리했는데, 세마포어는 하나 이상의 스레드가 critical section에 진입할 수 있도록 관리하므로, value의 값은 진입할 수 있는 스레드의 수에 따라 달라질 수 있습니다.
✔️ 세마포어는 작업의 순서를 정해줄 때도 사용 가능하다!
세마포어만의 signal mechanism
을 사용하면 작업의 순서를 정해줄 수 있습니다.
두 개의 프로세스 P1, P2가 존재한다고 하죠.
프로세스 P1은 task1
을 한 후에 semaphore ⇒ signal()
을 호출하는 작업입니다.
프로세스 P2는 task2
를 한 후에 semaphore ⇒ wait()
를 호출하고, 그 후에 task3
을 하는 작업입니다.
⇒ 이 경우, task3
은 반드시 task1
이후에 일어나도록 순서가 보장됩니다.
이처럼 반드시 정해진 순서로 작업이 일어나야 하는 상황에도 세마포어를 사용할 수 있습니다.
✔️ Mutex VS Binary Semaphore
뮤텍스와 이진 세마포어는 같은 걸까요? 그렇지 않습니다.
- 뮤텍스는 락을 가진 스레드에서만 락을 해제할 수 있지만, 세마포어는
wait()
를 실행하는 스레드와signal()
을 실행하는 스레드가 다를 수 있습니다. - 뮤텍스는 priority inheritance 속성을 가지지만, 세마포어는 이 속성을 가지지 않습니다.
상호 배제만 필요한 경우에 뮤텍스, 작업 간 실행 순서 동기화가 필요한 경우에는 세마포어가 적합합니다.
💋 참고자료
- https://www.baeldung.com/cs/spinlock-vs-semaphore
- https://www.baeldung.com/cs/what-is-mutex
- https://www.baeldung.com/java-mutex
- https://www.youtube.com/watch?v=gTkvX2Awj6g&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=5
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'Computer Science > Operating System' 카테고리의 다른 글
[OS] 데드락(Deadlock)은 언제 발생하고, OS, Java에서 어떻게 해결할까? (1) | 2023.11.22 |
---|---|
[OS] 동기화 메커니즘(Synchronization Mechanisms)(2): 모니터(Monitor) (0) | 2023.11.21 |
[OS] 동기화(synchronization)의 필요성: 경쟁 조건(race condition), 임계 영역(critical section) (0) | 2023.11.12 |
[OS] CPU Bound VS IO Bound: 스레드는 몇 개가 좋을까? (0) | 2023.11.10 |
[OS] 컨텍스트 스위칭: 프로세스 컨텍스트 스위칭 VS 스레드 컨텍스트 스위칭 (0) | 2023.11.09 |