스레드는 실행의 단위입니다. 스레드란 프로세스를 구성하는 실행의 흐름 단위를 의미합니다. 스레드라는 개념이 도입되면서 하나의 프로세스가 한 번에 여러 일을 동시에 처리할 수 있게 되었습니다.
스레드는 프로세스 내에서 각기 다른 스레드 ID, 프로그램 카운터의 값을 비롯한 레지스타 값, 스택으로 구성됩니다.
여기서 중요한 점은 프로세스의 스레드들은 실행이 필요한 최소한의 정보(프로그램 카운터를 포함한 레지스터, 스택)만을 유지한 채 프로세스 자원을 공유하며 실행된다는 점입니다. 프로세스의 자원을 공유한다는 것이 스레드의 핵심입니다.
스레드에서 포함하고 있는 최소한의 정보는 위의 그림과 같습니다. 우선 스레드는 각각의 스레드가 고유해야 하기 때문에 TID, Thread ID 라는 고유 식별자를 갖고 있습니다. TID는 "어떤" 스레드가 실행 중인지를 나타내는 소프트웨어 식별자입니다.
스레드가 프로세스 내에서 독립적인 기능을 수행한다는 것은 독립적으로 함수를 호출함을 의미합니다. 이러한 독립적인 함수 호출을 위해서는 스레드가 프로세스로부터 독립적으로 Stack Memory를 할당 받아야하고 또한 각각 PC Register를 가지고 있어야 합니다.
스레드가 각각의 Stack Memory를 가져야 하는 이유는 각 스레드가 자신만의 함수 호출 체인을 가질 수 있고 각 함수가 지역 변수를 선언이 가능하고 같은 함수를 여러 스레드에서 동시에 실행할 수 있어야 하기 때문입니다.
PC 레지스터는 현재 실행 중인 명령어의 주소를 가리키는 CPU 레지스터입니다. 각 스레드는 독립적인 PC 레지스터를 가져야 합니다.
또한 문맥 교환, Context Switching은 프로세스에서만 일어나는 것이 아닌 스레드에서도 일어나기 때문에 스레드당 독립적인 PC 레지스터의 값을 가져야 합니다. 문맥 교환으로 인한 문제가 발생하지 않기 위해서는 CPU가 각 스레드가 마지막으로 실행하던 위치를 기억해야 하기 때문에 PC 레지스터의 값을 독립적으로 갖고 있어야 합니다.
스레드와 프로세스는 본질적으로 동일한 개념, 즉 실행 컨텍스트(Context of Execution, COE) 에 불과합니다. 실행 컨텐스트, COE는 해당 컨텍스트의 모든 상태를 포함하는 개념입니다. 여기에는 다음과 같은 상태가 포함됩니다.
CPU 상태, 레지스터 등
메모리 관리 장치, MMU(Memory Management Unit) 상태, 페이지 매핑
권한 상태, UID(User Id), GID(Group Id)
다양한 통신 상태, 열려 있는 파일, 신호 핸들러등
리눅스에서의 접근 방식은 프로세스나 스레드라는 개념 자체가 존재하지 않는 다는 것입니다. 오직 실행 컨텍스트, COE만 존재하며, 리눅스에서는 이를 태스크, Task라고 부릅니다. 그리고 서로 다른 COE들은 필요에 따라 특정 컨텍스트를 공유할 수 있습니다.
전통적으로 UNIX에서는 프로세스를 생성하기 위하여 fork(), exec(), vfork() 명령어를 수행하여 작업을 수행하였습니다. 스레드의 경우 pthread_create()를 이용하여 스레드를 생성하였습니다.
리눅스는 핵심 시스템 콜인 clone() 모든 실행 단위(프로세스, 스레드 또는 하이브리드)를 생성하는 통합 시스템 콜을 이용하였습니다. clone()은 전통적인 유닉스에서 사용하는 방법에 비해 유연하다는 장점을 갖습니다.
CLONE_VM: 메모리 공간 공유
CLONE_FILES: 파일 디스크립터 공유
CLONE_FS: 파일시스템 정보 공유
CLONE_SIGHAND: 신호 핸들러 공유
유닉스는 위와 같이 작성한 플래그를 이용하여 어떤 컨텍스트를 공유할 것인지 정확하게 지정할 수 있습니다.
전통적인 프로세스와 스레드의 구분을 넘어 다양한 실행 컨텍스트 조합을 만들 수 있습니다. 리눅스에서는 프로세스와 스레드는 본질적으로 동일한 개념이며, 단순히 실행 컨텍스트의 일부만을 공유하는 방식이 다를 뿐입니다. 하지만 모든 시스템이 이를 구분하지 않는 것은 아니기 때문에 이 둘의 개념에 대해서 학습하는 것은 중요합니다.
프로세스의 문맥 교환
문맥 교환은 프로세스에서도 발생하고 스레드에서도 발생을 합니다. 두 문맥 교환은 모두 여러 개의 작업을 효율적으로 처리하기 위한다는 목적과 방법은 비슷하지만 교환시에 저장되고 복원되는 상태 정보에 있어서 큰 차이가 있습니다.
프로세스 문맥 교환의 경우 프로세스 전체 상태를 저장하고 복원 시켜야 합니다. 각 프로세스는 독립적인 메모리 주소 공간을 가지므로, 문맥 교환시 메모리 관리 장치(MMU) 의 설정을 변경해야 하는데 이는 페이지 테이블의 교체를 의미합니다. 또한 PCB 또한 저장 복원되어야 하는데 PCB에는 프로세스 ID, 레지스터 값, 스케줄링 정보, 메모리 관리 정보, 계정 정보, I/O 상태 정보 등이 포함됩니다. 시스템 자원 또한 전환을 해야 합니다. 파일 디스크립터, 신호 핸들러, 네트워크 소켓 등 프로세스에 할당된 시스템 자원에 대한 정보를 저장/복원해야 합니다.
스레드의 문맥 교환
이에 비해 스레드 문맥 교환은 같은 프로세스 내에서 실행되는 스레드 간의 전환을 의미합니다. 스레드는 프로세스 내에서 메모리 공간과 시스템 자원을 공유하기 때문에, 문맥 교환 시 프로세스에 비해 적은 양의 데이터를 저장, 복원을 수행하면 됩니다.
스레드 문맥 교환은 메모리 주소 공간을 유지합니다. 스레드 간 전환 시 메모리 주소 공간이 변경되지 않으므로 MMU 설정 변경이나 TLB 플러시가 필요하지 않습니다. 이는 스레드 문맥 교환의 주요 성능 이점 중 하나입니다.
스레드도 PCB와 같이 TCB가 존재하는데 TCB는 PCB보다 훨씬 작으며, 주로 스레드 ID, 레지스터 값, 스케줄링 정보, 스레드 로컬 스토리지에 대한 포인터 등을 포함합니다.
프로세스와 스레드 문맥 교환의 주요 차이점
프로세스 문맥 교환은 메모리 주소 공간 전환, TLB 플러시, 캐시 무효화 등으로 인해 상당한 오버헤드를 발생시킵니다. 스레드 문맥 교환은 최소한의 레지스터 세트만 저장/복원하므로 비용이 훨씬 적습니다. 일반적으로 프로세스 문맥 교환보다 5-10배 빠릅니다.
캐시 효율성에 있어서도 차이가 있습니다. 프로세스 문맥 교환의 경우 일반적으로 캐시 무효화가 발생하여 후속 캐시 미스가 증가할 확률이 높습니다. 반면 스레드는 동일한 프로세스 내에서 교환이 이루어지기 때문에 캐시 데이터가 여전히 유효할 가능성이 높아 캐시 히트율을 유지할 수 있습니다.
멀티 프로세스와 멀티스레드
멀티 프로세스의 경우 프로세스, PCB의 데이터들을 fork하여 복제하는 방식으로 작업을 수행한다고 가정할 경우(물론 이외의 경우에도) 프로세스 간의 자원의 공유가 기본적으로 공유하지 않는다는 특징을 갖고 있습니다.
멀티 스레드는 한 프로세스 내에서 여러 개의 스레드를 실행하며 작업을 수행하는 방법이기 때문에 기본적으로 자원을 공유한다는 특징을 갖습니다. 물론 각각의 스레드가 따로 가지는 자원들 (서 설명했던 독립적인 스레드를 위한 데이터) 가 존재하지만 이 특징은 멀티프로세스와 구분되는 가장 큰 특징입니다.
두 방법은 동시에 여러 작업을 수행한다는 측면에서 유사한 면이 있습니다. 적용할 시스템에 따라 두 방법의 장단점을 고려하여 적합한 방법을 수행해야 합니다. 메모리 구분이 필요할때는 multi process가 유리합니다. 반면에 문맥 교환, Context Switcing이 자주 일어나고 데이터의 공유가 빈번한 경우에는 멀티 스레딩을 사용하는 것이 더 유리합니다.
프로세스 동기화
동시 다발적으로 수행되는 프로세스들은 공동의 목적을 올바르게 수행하기 위해 서로 협력하며 영향을 주고 받기도 해야 합니다. 이렇게 협력하여 실행되는 프로세스들은 실행 순서와 자원의 일관성을 보장하기 위해서는 동기화가 되어야 합니다.
동시 다발적으로 수행되는 프로세스의 예시로 워드 프로세서에서 사용자로부터 입력을 받는 프로세스와 입력한 내용의 맞춤법을 검사하는 프로세스, 입력한 내용을 화면에 출력해주는 프로세들이 동시다발적으로 수행될 수 있고 이 프로세스들은 독립적인 프로세스이지만 공동의 목표를 위해서 서로 협력이 이루어져야 하는 작업들입니다.
프로세스의 동기화는 프로세스들 사이의 수행 시기를 맞추는 것을 의미합니다. 이를 위해서 프로세스를 올바른 순서대로 실행하기 위한 실행 순서 제어, 동시에 접근해서는 안되는 자원에 하나의 프로세스만 접근하게끔 하기 위해 상호 배제 를 프로세스의 동기화라고 정의합니다.
하지만 동기화는 프로세스만이 아닌 스레드 또한 동기화 대상입니다. 대상으로 하는 것이 아닌 실행의 흐름을 갖는 모든 것은 동기화의 대상입니다.
동기화 문제 대표적인 생산자, 소비자 문제를 코드를 이용하여 설명하며 해결해보겠습니다. 생산자 소비자 문제는 생산자가 자원을 추가하고 소비자가 자원을 소비하는 과정을 동시에 여러번 수행하였을 경우 총 자원의 총량이 변하지 않기를 기대하지만 작업을 수행할 경우 그렇지 않은 경우를 이야기 합니다.
위의 결과를 통해 확인할 수 있지만 기대한 결과 값과는 굉장히 차이가 있다는 것을 확인할 수 있습니다. 위의 코드는 스레드가 동시에 수행이 될 경우 공용 자원에 동시에 접근함으로 인해서임계 구역 문제가 발생하여 생긴 일입니다.
임계구역이란 여러 스레드가 동시에 접근하면 문제가 생길 수 있는 공유 자원이나 코드 영역을 이야기 합니다. 위의 코드에서는 sum 변수가 바로 그런 공유 자원입니다.
위의 작업은 단순합니다. 생산자 스레드가 sum++ 명령을 실행합니다. 소비자 스레드가 sum-- 명령을 실행합니다. 이 작업은 실제로 다음과 같은 세 단계로 이루어져 있습니다.
메모리에서 현재 sum 값을 읽기
값을 1 증가하거나 감소시키기
결과 값을 메모리에 저장하기 위한 문맥 교환
결과 값 메모리에 저장하기
위의 명령어들을 여러 스레드가 동시에 작업을 수행하게 되면 다음과 같은 문제가 생깁니다.
위와 같이 생산자의 작업이 소비자의 작업에 의해서 무시되고 최종 값은 9가 되게 되는 문제가 발생합니다. 이러한 상황은 공유 자원에 대한 접근을 모두 공정하게 갖고 있기 때문에 값의 업데이트에 대한 타이밍이 겹쳐서 발생하게 되는 경쟁 상태, (race condition) 문제가 발생하게 됩니다.
이러한 문제를 해결하는 방법은 여러 방법이 존재합니다. 대표적으로 뮤텍스 락, 세마포, 모니터와 같은 해결책이 있지만 이러한 해결 방법은 각각의 문제를 야기할 수 있기 때문에 한 가지만 적용하는 것이 아닌 필요성 등에 따라 적절한 동기화 기법을 선택해야 합니다.
뮤텍스(Mutext) 락
뮤택스는 MUTual EXclusion의 줄임말로 여러 스레드가 동시에 같은 자원에 접근하지 못하도록 막는 도구입니다. 뮤텍스는 잠금(lock)과 해제(unlock)라는 두 가지 기본 연산을 제공합니다.
뮤택스 락을 이용한 문제 해결 방법은 위와 같이 단순합니다. 뮤택스에서 제공해주는 lock()과 unlock()을 공용자원에 접근하여 값을 변경할 경우, 공용 자원의 사용이 끝났을 경우에 사용하여 문제를 해결하고 있습니다.
앞선 레이스 컨디션 문제는 임계구역에 여러 스레드가 동시에 접근을 하기 때문에 발생한 문제였습니다. 그래서 스레드가 임계구역에 진입하기 전에 뮤택스를 잠급니다. 뮤텍스가 잠기게 되면 현재 스레드는 잠금이 해제될 때까지 대기해야 합니다.
뮤텍스 락의 단점
뮤텍스는 본질적으로 "잠기" 또는 "잠금 해제"라는 두 가지 상태만을 가집니다. 이러한 이진적 특성으로 인해 한 번에 정확히 한 스레드만 임계 구역에 접근할 수 있다는 특징을 갖습니다. 이로 인해서 여러 스레드가 동시에 자원에 안전하게 접근을 할 수 있는 상황에서도 불필요한 제한이 생기게 됩니다.
또한 뮤텍스는 잠근 스레드만 해당 뮤텍스를 해제할 수 있습니다. 이러한 특성으로 인해서 예상치 못한 이유로 종료가 될 경우 해당 뮤텍스느 영원히 잠기는 상태 데드락 상태로 남을 수 있습니다.
우선순위 역전의 문제가 발생할 수 있습니다. 우선순위가 낮은 스레드가 뮤텍스를 잠근 상태에서 선점당하면, 우선순위가 높은 스레드는 계속 대기해야 합니다. 이는 시스템의 실시간 응답성에 안 좋은 영향을 줄 수 있습니다.
세마포(Semaphore)
앞서 뮤텍스 락은 하나의 자원에 여러개의 스레드가 접근하였을 경우를 상정하여 만들어진 동기화 도구 입니다. 하지만 세마포의 경우 공유 자원이 여러 개가 존재할 경우에 적용이 가능한 동기화 도구입니다.
세마포는 철도 신호기에서 유래한 단어입니다. 기차는 신호기가 내려가 있을 때는 멈춤 신호로 간주하고 잠시 멈춥니다. 반대로 신호기가 올라와 있을 때는 가도 좋다 는 신호로 간주하고 다시 움직이기 시작합니다. 세마포는 이와 같이 임계 구역 앞에서 멈춤 신호를 받으면 잠시 기다리고, 가도 좋다는 신호를 받으면 임계 구역으로 들어가게 됩니다. 세마포는 아래와 같은 특징을 갖습니다.
임계 구역에 진입할 수 있는 프로세스의 개수(사용 가능한 공유 자원의 개수)를 나타내는 전역 변수 S
세마포 값을 감소 시키고, 세마포의 값이 0이면 대기하는 P 연산, 혹은 wait, acquire
세마포 값을 증가 시키고, 대기 중인 스레드가 있을 경우 작업을 수행할 수 있게끔 신호를 주는 V연산 혹은 signal, release
교착 상태
교착 상태를 나타내는 가장 대표적인 문제는 식사하는 철학자 문제입니다.
식사하는 철학자 문제의 조건은 다음과 같습니다.
계속 생각을 하다가 왼쪽 포크가 사용이 가능해지면 왼쪽 포크를 집는다.
계속 생각을 하다가 오른쪽 포크가 사용이 가능해지면 오른쪽 포크를 집는다.
왼쪽과 오른쪽 포크를 모두 집어들면 정해진 시간동안 식사를 한다.
식사가 끝나면 오른쪽 포크를 내려 놓는다.
오른쪽 포크를 내려 놓은 뒤에는 왼쪽 포크를 내려 놓는다.
다시 처음 부터 반복한다.
위의 조건을 실행하면 철학자들은 모두 문제 없이 식사를 할 수 있을까요? 그렇지 않습니다. 철학자의 수가 적을 경우에는 식사가 가능할 수 있지만 철학자의 수가 늘어날 경우 식사를 하지 못하는 철학자가 발생할 수 밖에 없습니다.
이와 같은 문제를 일어나지 않을 사건을 기다림으로써 생기는 문제를 교착 상태 라고 정의합니다. 위의 문제에서는 교착 상태가 발생하기 위한 조건 또한 발견할 수 있습니다.
위의 문제에서 포크라는 자원은 서로 공유하여 사용할 수 없는 독점 자원이라는 특징을 갖고 있습니다. 철학자가 포크를 소유하고 있다면 다른 철학자는 그 포크를 사용할 수 없다는 특징, 상호 배제를 하기 때문입니다. 만약 포크를 여러 명이 동시에 사용이 가능하다면 위와 같은 문제는 발생하지 않았을 것입니다.
또한 다른 철학자가 포크를 사용할 때까지 기다려야 한다는 특징을 갖습니다. 이 철학자가 비선점형으로 포크를 사용한다는 특징이 있습니다. 만약 철학자가 자신이 포크가 필요할 경우 다른 철학자의 포크를 가져올 수 있다면 이러한 문제가 발생하지 않았을 것입니다.
비선점형으로 작업을 수행하기 때문에 점유와 대기 상태가 발생하는 것 또한 교착 상태에서 발견할 수 있는 특징입니다. 철학자가 식사를 하기 위해서는 왼쪽, 오른쪽 포크 모두 필요한 상태인데 한 철학자가 왼쪽 포크를 소유한 상태로 다른 철학자가 오른쪽 포크를 건네줄 때까지 기다리게 된다면 자원은 점유한 상태로 대기를 하고 있다는 문제가 발생합니다.
이러한 문제는 실제 컴퓨터 프로그램에 적용 시켜 보았을 경우 더욱 명확해집니다.
기본적으로 컴퓨터에서 사용되는 자원은 모두 제한적이고 사용하게 될 경우 점유하기 때문에 앞선 철학자의 문제를 위와 같이 적용할 수 있습니다. 이렇게 컴퓨터 자원을 이용하여 설명하였을 때 앞서 설명하지 않은 교착 상태에 또 다른 특징인 원형 대기 상태를 프로세스A, 프로세스D에서 확인할 수 있습니다.
위와 같이 프로세스가 요청 및 할당 받은 자원이 원의 형태를 이루었을 경우 교착 상태가 발생할 수 있는 가능성이 있습니다. 위와 같이 원형 상태로 지원 할당 그래프가 그려지지 않는다면 교착 상태가 발생하지 않지만 원형 상태가 된다고 해서 반드시 교착 상태가 발생하는 것은 아니라는 것을 주의해야 합니다.
교착 상태 해결 방법
교착 상태는 완벽하게 해결할 수 없는 근본적인 문제입니다. 이는 운영체제나 동시성 프로그래밍의 본질적인 특성과 관련이 있습니다. 컴퓨터 시스템은 본질적으로 제한된 자원을 여러 프로세스가 공유해야 한다는 특징을 갖습니다.
컴퓨터의 자원은 필연적으로 경쟁을 유발하는데 이는 많은 자원, 특히 물리적 장치는 본질적으로 하나의 프로세스만 사용할 수 있는 상호 배제를 피할 수 없기 때문입니다.
현대 시스템은 프로세스와 자원이 동적으로 생성이 되고 소멸하는 특징을 갖고 있습니다. 이러한 특징으로 인해서 사전에 가능한 모든 교착 상태의 시나리오를 예측하는 것은 불가능하고 이 문제는 시스템의 규모가 커질 수록 더욱 예측하기 어려워지기 때문에 교착 상태를 완전히 해결하는 것은 불가능한 문제입니다.
그렇기 때문에 교착 상태를 해결하기 위한 해결 방법은 회피 하거나 예방하거나 탐지 후 회복하거나 무시하는 방법을 시스템의 특성과 요구사항에 맞는 적절한 전략을 선택하고 조합함으로써 교착 상태를 관리해야만 합니다.
교착 상태 예방
교착 상태를 예방한다는 이야기는 교착 상태 발생의 조건 4가지 중 하나를 충족하지 못하게 한다는 것과 같습니다. 그렇기 때문에 각각의 조건이 발생하지 않게 하는 것이 가능한지를 살펴 보겠습니다.
가장 먼저 상호 배체를 해결할 수 있는지 살펴보겠습니다. 상호 배제를 해결하는 방법은 모든 자원을 동시에 접근이 가능하고 사용이 가능하게 하면 문제가 해결됩니다. 하지만 이는 현실적으로 불가능하므로 제외 됩니다.
다음으로 점유와 대기를 해결할 수 있는지 살펴 보겠습니다. 이 문제를 뮤텍스 락과 같이 생각해보겠습니다. 모든 자원은 할당이 가능하거나 할당이 가능하지 않는 상태만을 갖는다면 점유와 대기 문제를 해결할 수 있습니다. 하지만 이 문제는 앞서 이야기 했듯이 자원의 활용률이 낮아지기 때문에 효율적이라고 할 수 없습니다.
다음으로 비선점형을 해결할 수 있는지 살펴보겠습니다. 비선점형을 해결하는 방법은 모든 자원을 선점형으로 작업을 수행하게끔 하면 됩니다. 하지만 이 또한 현실적이지 못합니다. 프린터를 예시로 생각해보면 이미 프린터를 사용하고 출력하고 있는 작업이 있었는데 다른 작업이 자원을 빼앗아서 수행한다면 사용자는 원하는 결과물을 얻을 수 없기 때문에 이 또한 현실적이지 못합니다.
마지막으로 원형 대기를 해결할 수 있을지 살펴보겠습니다. 원형 대기는 수행할 작업에 순번을 매긴다면 발생하지 않습니다. 이 방법은 앞선 방법들에 비해서 현실적으로 수행이 가능한 해결 방법에 해당합니다.
데이터베이스 트랜잭션 시스템에서 여러 트랜잭션이 동시에 다수의 테이블에 접근하여 갱신 작업을 수행하는 경우, 각 트랜잭션이 서로 다른 순서로 테이블을 잠그면 교착 상태가 빈번하게 발생할 수 있습니다.
이 경우 모든 트랜잭션은 반드시 ID가 낮은 테이블 부터 잠가야 한다라는 규칙을 추가하게 될 경우 원형 대기 상태는 제거되어 교착 상태를 예방할 수 있습니다. 하지만 이 방법 또한 앞선 방법들에 비해 현실적일 뿐이지 모든 작업에 ID를 부여하는 것과 어떤 우선순위로 ID를 부여할 것인지 등에 복잡한 문제가 남아 있기 때문에 자원의 활용률이 떨어질 수 있다는 문제가 존재합니다.
이렇듯 교착 문제를 예방하는 완벽한 방법은 없기 때문에 상황과 환경을 분석하여 각 상황에 맞는 예방 방법을 사용하는 것이 최선이라고 할 수 있습니다.
교착 상태 회피
교착 상태를 회피하는 방법은 교착 상태를 만들지 않는 것입니다. 말만 들을 경우 굉장히 단순하지만 실상은 다른 해결 방법 중 가장 어려운 방법입니다. 교착 상태가 발생하는 이유는 사용이 가능한 자원이 한정적이라는 것입니다.
교착 상태 회피는 이러한 자원이 프로세스들에 배분할 수 있는 자원의 양을 고려하여 교착 상태가 발생하지 않을 정도의 양만큼만 자원을 배분하는 방법을 교착 상태 회피라고 합니다.
교착 상태를 회피하는 방법에 대해서 알기 위해서는 다음과 같은 상태를 이해해야 합니다.
안전 순서열 : 교착 상태 없이 안전하게 프로세스들에 자원을 할당할 수 있는 순서
안정 상태 : 교착 상태가 발생하지 않고 모든 프로세스가 정상적으로 자원을 할당 받고 자원을 종료할 수 있는 상태, 안전 순서열대로 작업을 수행하였을 경우 교착 상태가 발생하지 않는 상태
불안전 상태 : 교착 상태가 발생할 수도 있는 상태, 안전 순서열이 없는 상태
간단한 예시를 통해서 각각의 상태가 어떤 상태를 말하는 것인지, 어떻게 해결을 하는 것인지에 대해서 자세하게 다루어 보겠습니다.
위의 메모리는 추가 요청이 발생할 경우 작업을 제대로 처리할 수 없는 불안정한 상태입니다. 현재 메모리가 할당이 된 메모리는 10GB이고 가용이 가능한 메모리는 2GB입니다. 이 상태에서는 어떠한 작업의 조건도 만족 시킬 수 없기 때문에 교착 상태가 발생합니다. 이를 해결하기 위해서는 어떻게 해야 할까요?
각 작업이 시작할 때 최대 자원의 요구량을 선언하도록 합니다. 그리고 새로운 자원 요청이 들어올 경우 그 요청을 가상으로 승인해보고 시스템이 안전 상태를 유지하는지 확인한 후 안정 상태일 경우에는 요청을 승인하고 그렇지 않으면 거부하는 작업을 수행할 수 있습니다.
교착 상태 검출 후 회복
교착 상태가 발생 했음을 인정하고 사후에 조치하는 방법입니다. 검출 후 회복 방식에는 운영체제가 요구할 때마다 자원을 모두 할당하며, 교착 상태 발생 여부를 주기적으로 검사하여 교착 상태가 발생했을 경우 회복하는 방법을 사용하는 것입니다.
서비스C에서 교착이 발생 했음을 확인한 후 이를 회복하기 위하여 문제가 발생한 요청을 취소하고 다른 작업을 수행한 후 다시 요청을 하는 방법을 사용할 수 있습니다.
모든 서비스 요청에 타임아웃을 설정합니다. 요청 그래프를 주기적으로 분석하여 순환 의존성을 탐지합니다. 교착 상태가 탐지되면, 순환 고리 중 하나의 요청을 취소(타임아웃 처리)하고 지수 백오프로 재시도합니다.
이렇듯 문제가 발생한 프로세스를 강제로 종료하는 방법은 가장 단순하면서 확실한 방법입니다. 하지만 이러한 방법은 작업 내역을 잃게 될 가능성이 있고 또한 교착 상태의 상태를 반복하여 확인하는 과정에서 오버헤드가 발생할 수 있다는 문제가 생깁니다.
교착 상태 무시
교착 상태가 발생하면 특별한 예방 조치 없이 시스템을 운영합니다. 이 경우 사용자가 응답 없는 프로그램을 강제로 종료하거나, 최악의 경우에는 시스템을 재시작하는 방법을 사용할 수 있습니다. 이 방법은 오늘날 컴퓨터를 사용하면 응답이 없으니 재 시도를 하는 방법을 적용할 수 있습니다.
마무리 질문
Q : 프로세스와 스레드의 차이는 무엇인가요?
꼬리 질문
Q : 그렇다면 공통점은 무엇인가요?
Q : 스레드가 독립적인 스택 메모리와 PC 레지스터를 가져야 하는 이유는 무엇인가요?
Q : 멀티 프로세스와 멀티 스레드의 차이는 무엇인가요?
꼬리 질문
Q : 멀티 스레드가 멀티 프로세스보다 좋은 점은 무엇인가요?
Q : 멀티 스레드가 멀티 프로세스보다 안 좋은 점은 무엇인가요?
Q : 멀티스레드 환경에서 발생할 수 있는 동기화 문제와 그 해결책에 대해 설명해주세요.
꼬리 질문
Q : 레이스 컨디션(race condition)이란 무엇이며, 어떻게 발생하나요?
Q : 뮤텍스(mutex)와 세마포어(semaphore)의 차이점은 무엇인가요?
Q : 뮤텍스를 사용할 때 발생할 수 있는 데드락(deadlock) 상황을 예시와 함께 설명해주세요.
Q : 교착 상태(deadlock)란 무엇이며, 어떤 조건에서 발생하는지 설명해주세요.
꼬리 질문
Q : 교착 상태를 예방하는 방법과 회피하는 방법의 차이는 무엇인가요?
Q : 실제 시스템에서는 교착 상태를 어떻게 처리하나요?
Q : 교착 상태를 완벽히 해결하지 못하고 적절하게 예방하거나 회피, 무시해야 이유가 무엇일까요?
Q : 리눅스에서의 프로세스/ 스레드에 대한 접근 방법과 전통적인 프로세스/스레드 모델과 어떻게 다른지 설명해주세요.
=> 몰라도 되는 질문
리눅스에서는 프로세스와 스레드를 구분하지 않고 '실행 컨텍스트'라는 하나의 개념으로 통합하여 접근한다는 점을 설명해야 함
전통적인 프로세스/스레드의 개념에서 머물렀다면 Unix가 cd 명령어를 구현하기 어려웠다는 것을 설명하면 더 좋은 인상을 줄 수 있을 것이다
시간 →스레드 1: [실행]---[대기]------------[실행]--- ↓ ↑스레드 2: ---[대기]---[실행]---[대기]------- ↓ ↑스레드 3: -------[대기]------------[실행]---문맥 전환: PC 레지스터 값이 저장되고 복원됨
// 전통적인 프로세스 생성과 유사clone(0, ...);// 전통적인 스레드 생성과 유사clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);// 하이브리드: 메모리 공유하지만 파일은 독립적clone(CLONE_VM, ...);
초기 합계: 10producer, consumer 스레드 실행 이후 합계: -38292
프로세스A 프로세스B 현재 sum 현재 r1 현재 r2-------------------------------------------------------------r1 = sum | 10 10r1 = r1 + 1 | 10 11문맥교환 | 10 11 r2 = sum | 10 11 10 r2 = r2 -1 | 10 11 9 문맥교환 | 10 11 9sum = r1 | 11 11 9문맥 교환 | 11 11 9 sum = r2 | 9 11 9
트랜잭션 A: 테이블1(ID=1) 잠금 → 테이블2(ID=2) 잠금 요청 → 획득 → 작업 완료트랜잭션 B: 테이블1(ID=1) 잠금 요청 → 대기 → 트랜잭션 A 완료 후 획득 → 테이블2 잠금 → 작업 완료
작업 A: 현재 4GB 메모리 사용 중, 최대 8GB 필요작업 B: 현재 6GB 메모리 사용 중, 최대 10GB 필요총 가용 메모리: 12GB
작업 B가 2GB 추가 요청 → 가상 승인 결과: 남은 메모리 4GB(작업 A의 최대 요구량 채우기 불가능) → 불안전 상태 → 요청 거부작업 A가 추가 2GB 요청 → 가상 승인 결과: 남은 메모리 6GB(작업 B 최대 요구량 채우기 가능) → 안전 상태 → 요청 승인
서비스 A: 서비스 B에 요청 전송 → 응답 대기 중서비스 B: 서비스 C에 요청 전송 → 응답 대기 중서비스 C: 서비스 A에 요청 전송 → 응답 대기 중 (교착 상태 발생)
서비스 C의 요청이 타임아웃 → 서비스 C는 오류 응답 반환 → 서비스 B가 해당 응답 처리 → 서비스 A에 응답 → 순환 고리 끊김이후 서비스 C는 잠시 대기 후 서비스 A에 다시 요청 → 이번에는 서비스 A가 응답 가능 → 정상 처리
#include <iostream>#include <queue>#include <thread>void produce();void consume();int sum = 10;void produce() { for(int i = 0; i < 100000; i++) { sum++; }}void consume() { for(int i = 0; i < 100000; i++) { sum--; }}int main() { std::cout << "초기 합계: " << sum << std::endl; std::thread producer(produce); std::thread consumer(consume); producer.join(); consumer.join(); std::cout << "producer, consumer 스레드 실행 이후 합계: " << sum << std::endl; return 0;}
#include <mutex>std::mutex sum_mutex; // sum 변수를 보호할 뮤텍스를 선언합니다.void produce() { for(int i = 0; i < 100000; i++) { sum_mutex.lock(); sum++; sum_mutex.unlock(); }}