삽질의 현장/- 윈도우 시스템

#018_WIndow_System_쓰레드(thread) 란?

shovelman 2015. 9. 22. 00:35


안녕하세요 삽잡이입니다.

이번 시간에는 시스템 프로그래밍을 배우면서 가장 중요한 내용 중 하나!

쓰레드에 대해서 알아보도록 하겠습니다.


요즘은 멀티코어 시대로써 여러개의 코어... 

그러니까 CPU 자세하게는 연산 장치를 통해 프로그래밍을 할 수 있게 되었습니다.


우선 프로그램이 실행되기 위한 세가지 상태에 대해 알아보도록 하겠습니다.

1. Running 상태

2. Ready 상태

3. Blocking 상태


start를Ready전에, End를 Blocking 후에 실행되게 됩니다.

즉, 상태로는 크게 프로그램이 구동되고 실행되는 위의 세가지 상태로 나뉘게 된다는 것입니다.


프로그램이 실행되면 Ready 상태에 놓이게 됩니다.

그리고 자기가 실행할 순서가 되면 Running 상태에 놓이게 됩니다.

Running 상태에 놓일 수 있는 놈들을 코어의 개수와 같습니다.

예로 쿼드 코어면 Running 상태에 놓일 수 있는 놈은 4개,

듀얼이면 2개, 싱글이면 1개이겠지요...


Runnisng 상태로 놓이다가 시간이 지나면 다시 Ready로 되돌아 갑니다.

Running 상태로 돌아가는 상태로는 3가지가 있습니다.

1. Sleep

2. I/O

3. Wait


이 셋의 상태로 빠지게 되면 Blocking 상태로 빠지게 됩니다.

Blocking 상태가 되면 Running 상태로는 못갑니다.

오직 Running은 Ready 상태에서 가능합니다!


아무튼, Blocking 상태가 끝나면 Ready상태로 가게 됩니다.

물론, Ready에서 Blocking 상태로 가는 방법도 없습니다.


이것이 프로그램이 실행되는 가장 기본적인 개념입니다.


Ready 상태에는 우선순위 큐라고 관리하는 놈이 있습니다.

큐에서 바로 Running상태로 올리는 것이지요...


프로세스 단위로 실행이 아닌 쓰레드 단위로 실행시키는 것이기 때문에

앞으로는 프로세스의 실행 상태가 아닌, 쓰레드의 실행 상태라고 부르겠습니다.



쓰레드를 공부하다보면 명령어와 쓰레드 자체를 헷갈려하는 경우가 많습니다.

명령어와 쓰레드는 전혀 다른 것인데도 말입니다...

쓰레드는 명령을 실행하는 주체(커널 오브젝트)입니다.


Main()

{

명령 1;

명령 2;

명령 3;

........

}


이때에 한줄 한줄 실행시켜주는 놈이 있을 텐데 이를 primary 쓰레드라고 부릅니다.

main 함수를 실행시킬 수 있는 놈은 primary 쓰레드밖에 없습니다.


우리가 프로그램을 띄우자마자 프로세스가 뜨면

그 프로세스에는 꼭 Primary 쓰레드가 생성된다는 것입니다.

Primary 쓰레드가 종료되면 프로세스가 종료된다고 해도 과언이 아닐정도로 말입니다.

프라이머리 쓰레드의 종료 값이 프로세스의 종료값이 됩니다. 둘은 거의 한몸이죠...


어찌됬건,

프라이머리 쓰레드(PT)가 명령 중에 어딘가 실행을 할 때,

즉, 실질적으로 가능만 하다면 또 다른 Sub 쓰레드를 띄어서 다른 명령을 실행 시킬 수 있습니다.

물론, main에서는 실질적으로 명령 체계로써 어떻게 실행할 것인지를 나태낸것이라

좋은 방법은 아니지만요...


쓰레드는 다시 말하지만, 명령어를 실행시킬 수 있는 커널 오브젝트의 하나입니다.

다른 말로 실행 결로라고도 하지요...


 

이와 같이 CreateThread 함수를 통해 실행할 수 있는데요,

세번째 인수인 '쓰레드가 수행할 함수'와

네번째 인수인 '쓰레드 인자'가 중요합니다.


세번째 인수는 만들어낸 쓰레드가 실행할 명령어들을 가지고 있는 함수의 주소를 말합니다.

그 명령어는 이와 같이 사용될 수 있습니다.



이와 같은 형식으로 만들어진 쓰레드 함수의 주소를 인수로 받는 것입니다.

DWORD와 __stdcall, LPVOID 형으로 받은 pParam

같은 형식은 thread를 위한 함수의 형태로 반드시 지켜줘야하는 약속입니다.

즉, 항상 인자는 LPVOID 형, 리턴은 DWORD, 호출 규약은 __stdcall을 유지해야합니다.


쓰레드를 한개 실행할 때는 모르지만,

서브 쓰레드를 여러개 실행하게 되면 쓰레드는 각각 독립적으로 움직임으로 겹치게 실행되게 됩니다.

즉, 동시에 여러개의 작업이 실행되는 것입니다.


참고로, 



쓰레드를 생성할때 네번째 인자를 void 포인터 형식으로 받게 되는데,

이는 얼마든지 큰 값을 매개변수로 넘길 수 있게 하기 위해서입니다.


자.. 쓰레드의 특징들을 알아가보도록 하겠습니다.


우선, 쓰레드는 중립적인 실행 경로입니다.



이처럼 CreateThread를 실행할 경우에는 Primary Thread에 의해서 순차적으로 생성은 됩니다.

하지만, 생성이 된 다음 부터는 누구도 모릅니다...

코드가 실행 되는 것은 어떤 쓰레드가 먼저 실행될지는 모른다는 것입니다.


즉, 쓰레드가 생성된 후로는 누가 먼저 실행할 지는 모르게 되는 것입니다.

이를 누가 먼저 호출될지 모른다고 해서 비결정적(비동기적)이라고 합니다.


쓰레드에서는 C/C++ 개념에서 배웠던 내용과 비슷하게 명령은 공유하지만, 데이터는 공유하지 않습니다.

명령어와 데이터는 매우 중요합니다...


데이터는 Global Data 영역에 해당되는 전역, 정적, 상수 부분과

stack, heap 영역이 있습니다.


stack은 사실 쓰레드가 갖는 것입니다.

Global Data 영역과 heap영역을 프로그램이 갖는 것이구요...

프로세스의 메모리가 된다는 뜻입니다.

stack은 쓰레드의 메모리입니다. 프로세스의 메모리가 아님을 반드시 아셔야합니다.


쓰레드에는 크게 두 가지 종류의 메모리 공간이 있습니다.

하나는 stack, 하나는 TLS 입니다...


쓰레드들이 넘겨주는 인자들은 각 쓰레드의 stack에 저장됩니다.

Global Data 영역이나 heap 영역은 쓰레드가 가지지 않습니다.

이는 프로세스가 갖는 공유 메모리입니다.

쓰레드가 봤을 때에는 공유 메모리 영역이 되는 것이죠...


쓰레드를 생성할때에는 많은 정보들을 넘겨주기 위해 주소 형식으로 전달 할 수 있다고 했습니다.

 


이와같이 동적 메모리를 남겨줄 수도 있다는 것입니다.



static은 공유 메모리입니다.

따라서, 정적 변수에 쓰레드 번호를 저장하는 쓰레드 함수가 있다고 가정해보고

진행해보도록 하겠습니다.



다시 한번 말씀드리지만, static은 공유 메모리입니다.

따라서, 위의 쓰레드 함수를 출력하면 하나의 ID만이 출력되게 됩니다.

공유 메모리니까 덮어 쓰는 문제가 발생하기 때문입니다.


조금 자세하게 코드를 살펴보도록 하겠습니다.



위의 코드는 쓰레드 두개를 생성합니다.

main 함수에 있는 명령들은 primary thread에 의해 실행됨으로 순차적으로 생성될 것입니다.

하지만, 생성된 쓰레드가 'ThreadFunc' 함수를 실행 할 때에는 누가 먼저 실행될지 모르죠...


그런데, 위와 같이 static 변수를 저렇게 사용하게 되면,

변수로 넘어온 값이 static 변수인 num에 누적 저장되기 때문에

1, 2의 값중 하나는 덮어버리게 되는 문제가 발생합니다.


위의 예지와 같이 쓰레드 내에서는 static 변수를 함부로 사용해서는 안됩니다.

그렇다면, 함수가 종료되어도 변수가 살아있으며, 변수에 유효 범위가 함수 내로 한정 되어야하며,

쓰레드의 독립적인 변수가 필요합니다.

우리는 그 변수를 만드는 메모리를 thread local storage라고 부르며 

줄여서 'TLS'라고 부릅니다.


static 앞에 __declspec(thread) 이와 같이 붙이면 끝입니다...

즉, 쓰레드의 독립적인 메모리를 생성해 내는 키워드 입니다.


 


이처럼 만들지요...


stack 변수가 아니라 tls 메모리입니다.

tls는 쓰레드가 생성될 때 생성되고 종료될 때 종료되는 특징을 가지고 있습니다.


tls에는 크게 두가지가 있는데, 정적 tls와 동적 tls입니다.

원하는 시점에 메모리를 할당할 수 있다는 뜻으로 들립니다..


혹시 여러분은 슬롯이라는 말을 들어보셨습니까?

컴퓨터를 열어보면 똑같이 인터페이스를 꼽을 수 있는 곳 있지 않습니까...

그 곳을 카드 슬롯이라고 부르는데 어디에 꼽아도 동작하게 됩니다.


모든 넘버링이 어떤 위치에서나(메모리, 하드웨어) 동일하게 구성되어있는 구조를 슬롯이라고 부릅니다.

이와 같이 슬롯은 

일정한 주소의 위치, 넘버링 붙어있는 일정한 형태에 반복적인 패턴을 말합니다.


동적 tls가 슬롯 방식으로 되어있습니다. tls가 슬롯 방식을 사용한다는 것입니다.

왜 이런 방식을 사용할까요?

쓰레드가 전혀 독립적인 함수를 사용한다면 상관이 없겠지만,

A 쓰레드, B 쓰레드가 모두 C라는 함수를 공유한다고 해봅시다.


C라는 함수에서는 명령을 똑같이 만들고 싶을것입니다.

그런데 만들어지는 메모리 공간은 독립적으로 쓰기를 원하겠지요...


무슨 말이냐 하면... 같은 명령어 체계를 사용하는 여러 쓰레드가 있다고 해봅시다.

그런데, 그 쓰레드들도 독립적인 메모리 공간을 사용하고 싶을 때

슬롯 방식을 사용한다 이겁니다.


C함수에 '4번에 23을 넣어라!' 라고 명령이 정의되어있다고 해봅시다.

A, B 함수 등 어떤 쓰레드도 4번에 23이라는 값을 넣지 않겠습니까?

그런데 슬롯 방식을 사용하게 되면,

메모리를 독립적으로 유지하면서 하나의 명령어 체계로 사용할 수 있다는 장점을 갖는 것입니다.


흠... 똑같은 위치에 명령을 실행하기 위해서는

위치를 어디로 해야할 지 할당해야할 필요성이 있습니다.

이때에 TlsSetValue 함수를 사용하게 됩니다.

이 함수는 첫번째 인자에 두번째 인자를 Set해주는 기능을 가지고 있습니다.

위치를 어디로 할지에 대한 필요성은 TlsAlloc 함수가 해결해주죠...


tls는 메모리를 반환하는 것이 아닌, 인덱스 번호를 반환하게 되어있어서

슬롯을 제거할 때에는 할당 받은 인덱스 번호를 반환하도록

TlsFree 함수를 사용합니다.


이와같이 동적 tls를 사용하게 되면,

명령어 체계를 사용하지 않고도 서로 다른 쓰레드의 메모리를 사용할 수 있게 됩니다.



쓰레드 내에서 전역함수를 함부로 사용하게 되면,

전역함수를 여러 쓰레드 들이 사용하게 되어 위험해질 수 있습니다.


쓰레드는 띄우고 죽이는 것은 어렵지 않지만,

쓰레드를 안전하고 잘 사용하는 것이 어렵습니다.

즉, 내가 그 쓰레드를 원하는 형태로 동작시키는 것이 어렵다는 것입니다.


아무튼...

앞에서 쓰레드 내 전역함수를 함부로 쓸때에 위험할 수 있다고 했었습니다.

하지만, 전역변수를 쓰는 것보다 더 위험한 상황이 있습니다.

바로, '다른 쓰레드의 스텍 메모리에 접근하는 것'입니다.


스택은 쓰레드가 만들고 제거합니다.

쓰레드가 함수를 호출하면 스텍에 쌓이고, 리턴하면 제거하게 되지요...


이런 말이 있습니다.

'사라지려고 하는 혹은, 생명을 보장하는 메모리를 참조해서는 안된다.' 또한, '참조를 리턴하지말아라.'

이런말을 들어보신적이 있나요... 허허...

아무튼... 주소, &를 리턴하지 말라는 것입니다.


자기 쓰레드내에서 자기 스택변수로 참조를 받는것은 상관없지만,

즉, 스택에 아랫쪽 참조는 받을지언정, 윗쪽 참조는 안된다는 것입니다.

왜냐하면, 유효성을 보장받을 수 없기 때문입니다.

그래서 이런 상황에서 참조를 반환하지 말라는 것입니다.

만약 윗쪽 참조를 했다고 해봅시다. 반환을 통해 값을 전달 받았는데,

윗쪽 참조했던 함수가 종료되면 어떻게 됩니까...

그 참조는 아무런 의미를 갖지 않게 되지 않겠습니까...


아무튼... 하나의 쓰레드에서 자신의 참조를 갖는것은 가능하지만...

그 외에는... 조심하셔야합니다.


자... 다시 본론으로...

쓰레드들은 독립적이기 때문에 다른 쓰레드가 언제 소멸될지 모릅니다.

따라서, 쓰레드간의 메모리 참조는 정말 조심하여야합니다.

아니, 스택 메모리는 다른 스택 메모리를 절대 참조해서는 안됩니다.


전역으로 메모리를 사용하게 되면, 메모리 하나에 값들을 참조하는데,

한 쓰레드가 메모리를 참조하기 전에 전역 값이 바뀌면 다른 쓰레드들은 바뀐 값을 참조하게 됩니다.

이 또한 문제입니다...


쓰레드는 굉장히 중요합니다.

그런데, 쓰레드를 만들고 제거하는 것보다 어렵고 중요한 부분이 이와같이 

메모리를 신경써야된다는 것입니다.


다시 말씀드리지만,

언제 만들고 언제 사라질지 모르는 스택을 참조하는 것은 절대로 안됩니다.

또한 전역을 사용할 때에도 문제가 있으니 조심하셔야 합니다.


쓰레드에서 가장 중요한 것이 쓰레드 공유 메모리입니다.

쓰레드가 공유해야하는 데이터들이 굉장히 많기 때문입니다.

...


다음 시간에는 쓰레드의 공유 메모리에 관해 알아보도록 하겠습니다.

이상 삽잡이였습니다!