복습
스레드를 보기전에 지금까지 이해했던 내용들을 처음부터 간단하게 다시 정리해보자면 프로그램은 운영체제 위에서 동작한다고 했습니다. 그리고 이렇게 프로그램이 메모리 영역에 올라와서 실행되고 있는것을 우리는 프로세스라고 부르기로 했죠.
또한 운영체제는 이러한 프로세스에게 시스템 자원을 사용할 수 있도록 해주었고요. 그리고 프로세스 구조의 메모리 영역으로 크게 4가지 영역이 있다고 알고있습니다.
또 프로세스는 독립적인 개체로 다른 프로세스와 직접적인 커뮤니케이션이 불가능하죠. 그래서 운영체제 커널 영역을 통한 IPC 기법으로 프로세스간의 커뮤니케이션을 진행한다고 했습니다.프로그램과 프로세스
프로그램과 프로세스를 조금 더 이야기하자면 하나의 프로그램은 여러가지 작업을 동시에 진행해야 하는 경우가 매우 많죠. 그래서 하나의 프로그램은 다수의 프로세스를 생성하고 이러한 프로세스들은 컨텍스트 스위칭을 통해 계속해서 반복되죠. 그리고 운영체제 위에서 이러한 프로세스들이 실행 중인 것이고요.
즉, 프로세스는 운영체제 입장에서 보자면 실행해야하는 가장 작은 작업의 단위인 것이죠. 조금 더 자세히 말하자면 운영체제로부터 시스템 자원(독립적인 메모리 영역, CPU 사용 시간 등등)을 할당받은 작업의 단위라고 말할 수 있겠네요.
여기까지 간단하게 다시 살펴본 프로그램의 실행이고요. 이제 스레드를 보겠습니다.
Light Weight Process 라고 하기도 합니다.
위에 말대로라면 프로그램은 반드시 하나 이상의 프로세스를 가지고 있어야 합니다. 그리고 프로세스는 반드시 하나 이상의 스레드를 가지고 있기도 합니다. 다시 말해 프로세스의 생성은 스레드의 생성이라고 볼 수 있습니다. 이를 우리는 메인 스레드라고 부릅니다.
이러한 스레드의 특징은 하나의 프로세스 안에서 동작하는 흐름으로 프로세스가 할당 받는 4가지 영역 중 스택 영역을 별로도 할당 받고 나머지 영역들은 공유할 수 있습니다. 다시 말해 프로세스안에서 스레드끼리는 모든 자원을 공유할 수 있다는 말입니다.
이렇게 독립적인 프로세스는 다른 프로세스에 접근할 수 없지만 스레드는 가능하다는 큰 차이가 있습니다. 그리고 이러한 스레드는 하나의 프로세스안에 여러개를 생성할 수 있으며 이들을 동시에 실행할 수도 있습니다.
앞에서 스레드들은 별도의 스택 영역을 할당받는다고 이야기 했는데요. 그렇기 때문에 각 스레드별로 별도의 PC와 SP를 가지고 있습니다.
스레드는 왜 생겼을까요? 만약 프로그램을 더 빨리 실행하고 싶다면 멀티 프로세싱의 병렬 실행을 통해서 프로그램을 더 빨리 실행할 수 있는데 왜 스레드가 필요한지 의문점이 생길 수 있습니다. 그래서 한번 비교해 보겠습니다.
앞에서 이야기 했지만 프로세스는 4Gb의 메모리 공간을 사용한다고 했습니다.(다시 얘기하지만 실제 물리 메모리 공간은 아닙니다.) 만약 멀티 프로세싱을 위해서 여러개의 프로세스를 사용한다면 n * 4Gb의 메모리 공간이 필요할 것입니다.
하지만 스레드는 하나의 프로세스안에서 사용하기 때문에 다수의 스레드를 생성한다고 해서 별도의 메모리 공간이 필요하지 않습니다. 우선 메모리 사용(자원)면에서는 스레드의 승리입니다.
속도면에서는 어떨까요? 각 프로세스들이 자원을 공유하기 위해서는 IPC 기법을 사용해야 합니다. 이는 사용자 영역과 커널 영역을 번거롭게 반복해여 한다는 의미입니다. 스레드는 어떨까요? 하나의 프로세스안에서 활동하기 때문에 별도의 기능 없이도 프로세스의 모든 자원에 접근이 가능합니다. 속도, 다른 말로 사용자에 대한 응답성에서도 스레드가 우세한듯 합니다.
이렇게만 봤다면 다수의 프로세스를 만드는 것보다 하나의 프로세스안에 다수의 스레드를 사용하는게 더 좋아보입니다. 그러면 멀티 스레드가 더 좋을까요? 무조건 그런건 아닙니다.
다수의 프로세스를 사용한 멀티 프로세싱의 경우 하나의 프로세스에 문제가 생겨도 독립적인 특성을 가지고 있기 때문에 다른 프로세스에는 문제가 없습니다. 하지만 스레드의 경우 자원을 공유한다고 이야기 했었죠. 다시 생각해보면 하나의 스레드에 문제가 생길 경우 공유 자원을 사용하는 모든 스레드에 문제가 있을것이고 이는 해당 프로세스 전체에 문제가 발생하는것과 동일합니다.
그리고 리눅스 운영체제 환경에서 스레드 또한 프로세스와 동일하게 다뤄집니다. 즉, 많이 생성하게 되면 모든 스레드를 스케줄링해야 하기 때문에 컨텍스트 스위칭이 빈번하게 일어나고 이는 성능 저하로 이어집니다.
사실 다수의 프로세싱의 컨텍스트 스위칭에 의해 CPU에 부담을 덜어주기 위해서 스레드가 생겼다는 말들도 있습니다. 하지만 아이러니하게도 이러한 스레드들도 많이 사용하게 되면 똑같이 성능 저하가 생기니..
이러한 스레드를 잘 사용한다면 CPU와 전체적인 프로그램의 성능 그리고 사용자에 대한 응답성까지 향상시키며 효율적으로 자원을 공유할 수 있습니다. 하지만 단일 스레드의 문제가 프로세스 전체에 영향을 끼칠 수 있으며 다수의 스레드 생성으로 성능 저하가 일어날 수 있습니다.
하나의 프로세스 안에 다수의 스레드가 생성될 수 있고 이러한 스레드들이 모든 프로세스의 데이터에 접근할 수 있기 때문에 생기는 문제입니다.
다수의 스레드가 동일한 자원에 접근시 동기화 이슈가 발생하는데 간단하게 예를 들어보자면 동일한 자원을 다수의 스레드가 동시에 수정(접근)해서 각 스레드의 결과 값이 다르게 변할 수 있는 문제라고 생각할 수 있습니다.
이러한 동기화 이슈가 발생된다면 디버깅이 매우 어렵기 때문에 별도의 스레드 관리가 필요합니다. 다시 말해 동기화가 필요하다는 말이고 이는 스레드간의 실행 시기를 맞춰야 한다는 말이 됩니다.
동기화를 할 수 있는 방법 다시 말해 실행 시기를 맞출 수 있는 방법은 간단합니다. 동시에 스레드가 접근할 수 없도록 막으면 됩니다.
다수의 스레드가 접근해 변경할 수 있는 공유 변수에 대한 Exclusive Access를 만들고 한 스레드가 공유 변수를 갱신하는 동안 다른 스레드가 동시에 접근하지 못하도록 막으면 되는거죠. 파이썬 코드로 예를 들어보면 아래와 같습니다.
lock.acquire()
for i in range(10000); // 임계 영역 | critical section
g_count += 1 // 임계 자원 | critical resource
lock.release()
이렇게 임계 영역(critical section)에 대한 접근을 막기 위해서 LOCKING 메커니즘이 필요하며 해당 메커니즘은 크케 두 가지 방법으로 나눌 수 있습니다.
임계 영역을 설정하고 해당 임계 영역에는 스레드가 하나만 접근하거나 특정 개수만 접근할 수 있기 때문에 나머지는 기다리고 있어야 합니다. 이때 순서를 잘못 설정하면 어떠한 스레드도 임계 영역에 접근할 수 없고 기다리고 있는 상태가 됩니다. 혹은 특정 스레드들은 아예 임계 영역에 접근할 수 없게 되는데 이것을 교착상태(Deadlock)와 기아상태(Starvation)라고 합니다.
무한 대기 상태: 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에, 다음 단계로 진행하지 못하는 상태입니다. 이는 프로세스와 스레드 둘다 이와 같은 상태가 일어날 수 있습니다.
특정 프로세스의 우선순위가 낮아서 원하는 자원을 계속 할당 받지 못하는 상태입니다.
교착상태는 여러 프로세스가 동일 자원 점유를 요청할 때 발생하지만 기아상태는 여러 프로세스가 부족한 자원을 점유하기 위해 경쟁할 때, 특정 프로세스는 영원히 자원 할당이 안되는 경우를 주로 의미합니다.