멀티 스레딩이란 말 그대로 한 프로세스 내에서
여러 실행 흐름(스레드)을 동시에 실행하여
작업을 병렬적으로 처리하는 기법이다.
요즘 같이 cpu의 성능이 좋아진 시대에서 cpu의 성능을 최대한 끌어내려면
멀티 스레딩은 어쩌면 필수라고도 볼 수 있다.
그렇다면 스레드가 뭐길래 병렬적으로 실행 가능하다는 걸까?
스레드에 대해 다들 알겠지만 조금 더 깊이 들어가보자.
스레드는 크게 user-level thread
와 kernel-level thread
두 가지 방식으로 구현된다.
우리가 보통 사용하는 스레드는 사용자 수준 스레드이며, 스레드 라이브러리에 의해 관리된다.
이것만으로는 실제 병렬
적으로 동작하지 않고 병행
으로 눈속임만 할 뿐이다.
실제 병렬적으로 동작할 수 있는건 커널 수준 스레드이고
사용자 수준 스레드를 병렬적으로 동작하게 하려면
커널 수준 스레드와 매핑을 시켜야 한다.
사용자 수준 스레드는 진짜 스레드가 아니기 때문에
만약 한 스레드가 블로킹 호출을 하면 다른 스레드들도 블로킹 된다.
(사용자 수준 스레드는 실제 병렬이 아닌 병행으로 동작하고 있었기 때문에)
커널 수준 스레드는 커널이 관리하며, 각 스레드는 커널 객체로 존재한다.
커널 수준 스레드는 하드웨어의 프로세서
나 코어
에 대응되어 진정한 병렬 실행이 가능하다.
일단 커널 수준 스레드는 말 그대로 커널 수준이기 때문에
사용할 때마다 커널 모드와 사용자 모드 간의 전환이 일어난다.
이는 시간과 자원을 소모하는 작업이다.
또한 실제 하드웨어에 대응되기 때문에 많은 스레드를 생성하면 많은 자원이 소모되며,
실제 물리적인 코어 개수만큼의 스레드까지밖에 생성하지 못한다.
이를 추상화하여 나온게 사용자 수준 스레드로,
이는 진정한 스레드는 아니지만 실제 커널 수준 스레드와 N:M 매핑되어
개발자가 느끼기에는 실제 스레드처럼 사용할 수 있다.
또한 이는 가상이기 때문에 개수에 한계가 없고,
아무리 많이 만들어도 커널의 자원을 사용하지 않는다.
(물론 너무 많이 만들면 컨텍스트 스위칭만 하다가 끝나게 된다)
스레드는 하나의 프로세스 내에서 프로세스 메모리의 코드 영역, 데이터 영역, 힙 영역을 공유하며,
개별적인 program counter와 stack, TCB(스레드 제어 블록)등을 가진다.
이러한 특성때문에 스레드 간에는 빠른 통신과 데이터 공유가 가능하지만,
동기화와 race condition같은 문제를 야기할 수 있다.
여기서 스택 영역은 각 스레드가 독립적으로 가지는데,
이 때문에 지역 변수와 같은 스택에 쌓이는 데이터에 대해선
동기화 문제를 신경 쓸 필요가 없다.
이와 달리 힙 영역의 객체들이나 전역 변수 등에 write 작업을 할 시에는 동기화 문제를 신경 써야 하는데,
이를 방지하기 위한 여러 장치들이 존재한다.
뮤텍스는 상호 배제라는 이름처럼 공유 자원에 대한 접근을 제한하여
동시에 하나의 스레드만 공유 자원에 접근할 수 있게 하는 장치이다.
이는 커널 수준에서 구현되며, 잠금과 해제를 통해 임계 구역을 보호한다.
뮤텍스는 내부적으로 소유자와 상태, 대기 큐를 가지는 자료구조로,
현재 뮤텍스를 소유하고 있는 스레드의 ID를 저장한다.
뮤텍스가 잠긴 동안 접근을 시도한 스레드는 대기 큐에 넣어
뮤텍스가 해제 됐을 때 대기 중인 스레드 중 하나를 깨운다.
실제 뮤텍스를 이용한 공유자원 접근 시나리오를 살펴보면
뮤텍스는 간단한 동작 원리로 동기화를 이뤄냈지만,
높은 우선순위의 스레드가 낮은 우선순위의 스레드에 의해 블로킹될 수 있는
우선순위 역전 문제와 교착 상태가 발생할 수 있다.
이러한 문제들은 다른 동기화 장치들에서도 발생할 수 있는 문제들로
이를 방지하기 위한 다양한 방법들이 존재한다.
세마포어라는 용어는 철도 및 해상에서 신호를 보내는 장치에서 유래되었다.
컴퓨터 공학에서 세마포어는 공유자원에 대한 접근이 가능한지 알 수 있는 장치로써 사용된다.
세마포어는 정수 값을 사용하여 현재 접근 가능한 스레드 수를 나타내는 변수이다.
이 변수는 두 가지 원자적 연산을 통해 조작되는데
세마포어의 값을 감소시키는 P(wait)
연산과,
세마포어의 값을 증가시키는 V(post)
연산이 있다.
접근 시
P
연산을 통해 세마포어의 값을 감소시켜 공유 자원에 대한 접근을 시도한다. 사용 후
세마포어는 두 가지 종류로 나뉘는데
0과 1의 값을 가지는 Binary Semaphore
와
0 이상의 값을 가질 수 있는 Counting Semaphore
이다.
binary semaphore는 뮤텍스와 유사한 동작을 하지만 뮤텍스와 달리 소유자의 개념이 없다.
세마포어는 자원 접근의 수를 제어하는 데 유용하지만,
공유 자원에 대한 동시 쓰기 작업을 안전하게 수행하려면
뮤텍스와 같은 추가적인 동기화 메커니즘이 필요하다.
사실 나는 세마포어가 동기화 장치이기 때문에 당연히 모든 경쟁조건을 막아줄 것이라 생각했는데,
여러 스레드의 접근을 허용하면서 어떻게 이게 가능할까 했는데
알고보니 못하고 있었다..!!
그렇다면 세마포어는 왜 쓰는거지? 라는 의문이 들었는데
약간 다이어트 보조제처럼 "체중감소에 도움을 줄 수 있음" 같은 느낌이었다.
여기까지 멀티 스레딩에 대해 정리해보았고
동기화 장치에 monitor나 condition varaiable등이 더 있는데
다음에 궁금하면 또 정리해보겠다..