우리에게 필요한 것은 무엇인가?
앞 장인 04. 캐시라인 크기 경계에서는 캐시라인 경계를 초과하는 큰 메모리를 포인터로 접근하려 할 때 생길 수 있는 오차를 알아봤다. 그러나 이것은 x86계열의 CPU에서 발생할 수 있는 문제이다. 다음은 CPU 아키택쳐 별 메모리처리에 대한 자료이다.
CPU 아키택쳐 별 메모리 오더링 자료를 보면 x86 CPU별로 메모리 오더링에 대한 처리가 다른것을 볼 수 있다. 그렇다면 우리는 프로그래밍과 컴파일러 말고도 CPU 아키택쳐까지 고려하여 프로그래밍 해야 하는가? 과연 그러한 것이 가능할까? 이 장에서는 버그 없는 멀티스레드 프로그래밍을 위해 필요한 것들에 대해 알아보자.
원자적 메모리 Atomic Memory
02. 데이터 레이스 장 에서 잠깐 언급한 원자성 Atomic이란 더이상 쪼개지지 않는 최소 단위를 의미한다.
원자적 메모리란 어떤 스레드가 메모리를 참조하여 처리하는 일련의 과정이 더 이상 쪼개질 수 없는 최소 단위로 일어나는 메모리를 뜻한다.
원자적 메모리는 메모리를 갱신하는 모든 부분에 Lock을 걸거나, C++11의 <atomic>
에서 제공하는 타입(ex: atomic_int)을 사용하는 것으로 구현할 수 있다. 그러나 이러한 방법은 막대한 오버헤드가 발생할 뿐만 아니라 실제 프로그래밍에서는 변수 하나하나의 원자성보다 더 큰 단위의 원자성이 필요하다.
원자적 자료구조
원자적 자료구조는 자료구조가 갱신되는 일련의 과정이 더 이상 쪼개질 수 없는 최소 단위로 일어나는 자료구조를 뜻한다. 이렇게 되면 자료구조에 접근하는 모든 스레드들의 순서가 지켜지게 되어 멀티스레드 프로그래밍에서 발생할 수 있는 많은 문제가 해결된다. 우리가 익숙하게 사용하고 있는 자료구조인 STL컨테이너는 원자적 자료구조가 아니기 때문에 스레드 안전이 보장되지 않는다. 이 말은 멀티스레드 프로그래밍에서 STL을 그냥 사용하면 반드시 문제가 발생한다는 뜻이다.
블로킹 알고리즘의 문제점
원자적 자료구조를 구현하기 위해서 기존의 자료구조에 Lock을 사용하면 어떨까? 이러한 문제 해결방법을 블로킹 알고리즘이라고 한다. 실제로 블로킹 알고리즘은 때때로 필요하고, 사용하기 편하다는 장점도 있다. 하지만 정확한 부분에 제대로 알고 사용하지 않는다면 그 편리함은 양날의 칼이 되어 돌아오게 될 것이다. 뿐만 아니라 Lock을 사용하지 않았을 때는 발생할 수 없었던 새로운 문제들에 직면하게 되며 디버깅은 커녕 제현하기조차 어려운 악성 버그로 탄생하게 된다.
교착 상태 Deadlock
개요
두 개 이상의 스레드가 서로 상대바의 작업이 끝나기만을 기다리고 있는 상태. 결과적으로 아무것도 완료되지 못하는 상태를 뜻한다.
예제
교착 상태는 다음 네 가지 조건이 모두 충족될 때 발생한다.
- 상호 배제(Mutual exclusion): 스레드들이 필요한 자원을 서로 독점하려고 한다.
- 점유와 대기(Hold and wait): 어떤 스레드가 자원을 점유하면 다른 스레드들은 그 자원을 대기해야 한다.
- 비선점(No preemption): 자원은 선점 불가하며, 먼저 점유한 스레드가 끝나야 점유할 수 있다.
- 순환 대기(Circular wait): 각 스레드는 순환적으로 다음 스레드가 요구하는 자원을 가지고 있다. 위 3가지 조건만 충족하면 교착 상태가 발생 하거나 안할 수 있지만 이 조건으로 반드시 교착상태에 빠지게 된다.
기아 상태 Starvation
개요
스레드가 자원을 획득하지 못하고 작업을 완료하지 못하는 상태.
예제 - 식사하는 철학자 문제
- 5명의 철학자가 원탁에서 식사 한다.
- 테이블 중앙에 음식이 있으며, 포크는 5개이다.
- 배고픈 철학자는 왼쪽 포크를 먼저 잡은 후 오론쪽 포크를 잡으며, 두개의 포크를 모두 잡았을 때 식사를 시작한다.
- 식사를 마친 철학자는 두개의 포크를 내려놓고 생각을 한다.
- 철학자끼리는 서로 개입할 수 없다.
위와 같은 상황일 때 모든 철학자가 동시에 식사한다면 교착 상태가 발생하며 결국 어떤 철학자도 식사하지 못하는 기아 상태가 발생한다.
우선순위 역전 Priority Inversion
개요
Lock을 공유하는 덜 중요한 작업들이 중요한 작업의 실행을 막는 현상. 화성 탐사선인 패스파인더에서 테스크가 자원을 획득하지 못하고 기아에 빠지자 감시타이머가 시스템을 제부팅하여 해결하려고 하는 바람에 이 현상이 무한 반복된것으로 유명해졌다.
예제
기호 | 설명 |
---|---|
L | 자원이 필요한 저순위 스레드 |
M | 자원이 필요없는 중순위 스레드 |
H | 자윈이 필요한 고순위 스레드 |
R | Lock획득이 필요한 공유자원 |
- L 가장 먼저 시작해서 R을 획득
- H 나중에 시작해서 R을 획득 못하고 대기
- M 가장 늦게 시작했으나 R이 필요없고 L보다 우선순위가 높아 곧바로 수행
- M 작업 종료
- L 작업 재개 및 종료
- H가 R을 획득하여 작업 수행 및 종료
가장 우선순위가 높은 H가 가장 마지막에 처리되는 문제가 발생한다.
잠금 호송 Lock Convoy
개요
멀티스레드 상황에서 Lock을 점유한 스레드가 스케쥴 아웃되어 프로그램의 처리속도를 큰폭으로 저하시키는 현상.
예제
프로세서의 수보다 스레드 수가 지나치게 많아서 과도한 경함이 발생하고 있을 때, 문맥 전환 등으로 Lock을 획득한 스레드가 스케줄 아웃되면서 발생한다. 이렇게 되면 해당 Lock을 사용해야 하는 다른 스레드들은 스케줄 아웃된 스레드가 다시 스케쥴 인 되어 처리가 끝날 때 까지 대기해야 하기때문에 프로그램의 처리속도가 단순한 오버헤드와는 차원이 다르게 큰 폭으로 저하된다. 뿐만 아니라 기아상태나 교착상태와는 다르게 작업이 멈추지 않는다.