본문 바로가기
C/[서적] 뇌를 자극하는 윈도우즈 시프

Chapter 11. 쓰레드의 이해

by GameStudy 2022. 2. 6.

11.1 쓰레드란 무엇인가

 

Prologue) 11장에서는 쓰레드를 배워 볼 예정.

  다만, 프로세스의 내용을 되짚어보면서 쓰레드가 무엇인지 비교해보고

  왜 쓰레드가 필요한지를 생각해 보아야 함.

 

11.1-1 프로세스 Vs. 쓰레드

  Note) 멀티 프로세스가 필요한 경우

    OS 관점에서는 멀티 프로세스가 필요함.

    둘 이상의 프로그램을 실행하기 위해서임.

    근데, 지금까지는 대부분 하나의 프로그램 == 하나의 프로세스

    즉, 하나의 프로그램은 하나의 코드 실행 흐름을 가짐.

    앞으로 개발하다 보면 하나의 프로그램 안에서 둘 이상의 실행 흐름이

    필요한 경우가 생기게 됨.

    ex. 2인 전투 테트리스. 하나의 프로그램에 두 개의 실행 흐름이 생김.

      게임을 켰을때 대기방에선 혼자하다가 공방에선 프로세스를 하나 더 생성해서

      멀티 프로세스를 통한 전투 테트리스를 구현할 수도 있음.

    그럼 실행의 흐름이 2개가 되려면 어떤 요소들이 필요할까?

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 일단 하나의 프로세스에서 시작됨.

    따라서 Code/Data/Heap/Stack 영역이 할당됨.

    Code 영역: 프로그램의 코드가 올라가는 영역

    Data 영역: 전역 변수, 정적 변수가 올라가는 영역

    Heap 영역: 동적할당을 위한 영역

    Stack 영역: 지역 변수, 매개변수를 위한 영역

    멀티 프로세스를 하게되면, 부모 프로세스의 영역만큼이 또 그대로 할당됨.

    그리고 그들 사이의 CS이 매우 빈번하게 발생하게 됨.

    뿐만 아니라, OS 관점에서는 각 프로세스 리소스마다 커널 오브젝트가 생성되고

    또한 스케줄러가 프로세스 간의 실행 순서도 조절해주어야 함.

    하튼간에 할일이 엄청나게 많아짐. 시스템 입장에서는 아주 부담스러운 일.

 

  Note) 그럼 그냥 하나의 프로세스를 위한 메모리 영역에서 적절하게 공유하면 어떨까?

    공유하면 어떤 메모리를 공유하면 되고, 어떤건 안될까?

    일단 Code 영역 정도는 같이 써도 될듯. A 프로세스의 Code, B 프로세스의 Code, ...

    그리고 각자 코드 실행 흐름을 가짐. 이건 다시말해, 함수 호출이 발생한다는 것.

    따라서 Stack Frame이 각자 필요하게 됨.

    근데 Stack 영역을 같이 쓰면, 넣고 빼는데 있어서 너무 불편해짐.

    그냥 Stack 자료구조 하나가지고 두 가지의 데이터 흐름을 가진다고 상상해보면 암.

    Data 영역은 흐름별로 그렇게 많이 쓰는것도 아니라서, 공유가능.

    Heap 영역은 점유했으면 철저히 해제해준다는 가정하에, 공유가능.

    즉, 멀티 프로세스와는 다르게 쓰레드라는 개념을 만들어 냄.

    하나의 프로세스 내에서 둘 이상의 코드 실행 흐름을 적은 부담으로 만들어 낼 수 있음.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 프로세스란 앞서 정의한 바에 의하면 할당된 메모리 영역 + 레지스터 셋이라 함.

    쓰레드(Thread)를 정의하자면, 별도의 코드 실행 흐름을 만들기위해서

    프로세스의 Code/Data/Heap 영역은 공유하고, 별도의 실행 Code와 Stack 영역을 묶어서

    쓰레드라고 정의하면 됨. (이제 쓰레드라는 개념을 만들었으니 "부모"라는 단어는 빠짐.)

 

  Note) 위와 같은 쓰레드의 정의 덕분에 등장하는 특징들이 있음.

    이에 대해서 배워 볼 예정.

 

  Note) 이론 상으로는 이제 알거 같음. 근데 코드 상으로는 어떻게 구현될까?

    일단 Code 영역에는 함수가 올라간다고 생각하자.

    함수에는 전역 함수와 main() 함수가 있음. main() 함수가 곧 실행 흐름의 시작.

    즉, 실행 흐름이 둘 이상이라는건 main()이라는 이름의 함수가 두 개 이상이라는 것.

    예로 들어서, main1()은 프로세스가 생성되자마자 자동적으로 실행되고

    main2() / main3()도 누군가에 의해서 실행되는 것.

    이런 이야기를 하는 이유는, Code 영역이라는 메모리 공간만을 공유하는게 아니라

    Code 영역에 올라가 있는 리소스들도 공유함.

    예로 들어서 main2()가 쓰레드 A를 대표하는 실행 흐름이고

    main3()가 쓰레드 B를 대표하는 실행 흐름이라고 가정 해 보자.

    그리고 Code 영역에는 add()라는 전역 함수가 올라가 있음.

    이때 같은 Code 영역을 공유하기 때문에 add() 함수는 main1()/main2()/main3()에서

    모두 호출 가능함. 그러나, 멀티 프로세스에서는 부모 프로세스가 자식 프로세스의

    Code 영역에 존재하는 함수 호출이 불가능함. 반대로도 마찬가지. 

    프로세스 서로간의  함수 호출이 불가능함. 그래서 IPC 통신 기법들이 필요 했음.

    그러나 하나의 프로세스 내부 쓰레드 간의 전역 함수 호출은 가능함.

    뿐만아니라, Data영역/Heap영역도 공유하고 있음. 그래서 딱히 통신 기법들이 필요 없음.

    전역 변수/정적 변수/동적 할당 메모리를 내부 쓰레드 간의 공유가 가능함.

    그래서 결론적으로 '쓰레드가 좋은 거구나' 생각할 수 있음. 

    하지만 경우에 따라서는 달라짐. 멀티 프로세스를 적용하면 좋은 경우가 있고

    멀티 쓰레드를 적용하면 좋은 경우가 따로 있음. 

    옛날에는 "프로세스를 대체하는 것이 쓰레드다"라고 주창했지만 시대가 변함.

    이제 둘은 서로 다른 문제 혹은 경우를 해결할 때 쓰임.

    다만, 이 책에서는 대부분 멀티 프로세스를 가지고 해결하는 문제들이 대부분.

    나중에 TCP/IP 통신을 배우면 멀티 쓰레드는 쉽게 접하기 때문.

    지금까지의 쓰레드 내용은 윈도우즈 OS의 쓰레드 특성을 배운게 아님.

    아주 교과서적인 OS의 쓰레드 특성을 배움. 윈도우즈 OS의 쓰레드는 조금 다를 수 있음에 주의.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

    

11.1-2 프로세스와 쓰레드의 관계

  Note) 실질적인 Code 영역의 구조 도식화

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

 

 

11.1-3 Windows에서의 프로세스와 쓰레드

  Note) 이번 시간에는 Windows에서의 프로세스와 쓰레드에 대해 배움.

    아래 스케줄러는 윈도우즈의 스케줄러임.

    지금까지의 스케줄러의 스케줄링 대상은 프로세스였음.

    그러나 실질적으로 Windows OS의 스케줄링 대상은 쓰레드임.

    윈도우즈의 스케줄러는 아래 그림에 따르면 총 5개의 쓰레드를 스케줄링함.

    그러니까, 지금까지 배웟던 프로세스의 상태정보가 프로세스에 존재하지 않을 수 있음.

    윈도우즈에서는 프로세스가 아니라 쓰레드가 갖고 있음.

    대표적인 프로세스의 다섯 가지 상태: Create/Ready/Running/Blocked/Terminated

    프로세스 개념인 상태 정보, 우선순위, 동일순위(라운드로빈)가 쓰레드에 그대로 적용됨.

    그래서 이건 프로세스다 이건 쓰레드다 확실하게 나누기가 조금 애매해짐.

    간단하게 '프로세스 내에 존재하는 실행의 흐름이다.' 정도로 이해하면 좋을듯.

    그리고 구성하는 요소(위에 정의함.)를 기억하자.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 다시 말하면 윈도우즈에서 쓰레드는 실행 흐름임.

    즉, 실행 흐름의 주체는 프로세스가 아님에 주의. 

    일반적으로 소스코드를 작성해서 .exe 프로그램을 만들면 main() 함수가 실행됨.

    이 main() 함수도 결국엔 쓰레드에 의해 실행됨. 이 쓰레드를 특별히 메인 쓰레드라 함.

    즉, 지금까지는 하나의 쓰레드(실행흐름)를 가지는 프로그램을 작성해 왔던 것.

    거기에 우리는 둘 이상의 쓰레드(실행흐름)를 가지는 프로그램을 작성하기 위해 배우고 있는 것.

 

  Note) 과격하게는 "프로세스는 쓰레드를 담고 있는 껍질에 지나지 않는다."고도 함.

    너무 과격한 표현. 프로세스는 훨씬 큰 범주이고, 쓰레드는 그 속의 작은 내용.

    Code/Data/Heap 영역을 쓰레드라 할 수 있을까? No. 얘네들은 프로세스임.

    즉, 껍질이 아니라, "쓰레드가 실행될 수 있는 환경을 제공해 주는 것이 프로세스다" 라고 이해하자.

 

11.1-4 커널 레벨 쓰레드 Vs. 유저 레벨 쓰레드

  Note) 이번 절의 내용은 아주아주 중요함. 

    특히나 추후에 OS를 넘나드는 개발을 할 예정이라면 더 중요함.

    Windows OS의 특성들을 잘 기억해 두고 타 OS로 넘어가면 큰 도움이 됨.

 

  Note) 일단 OS를 나눠보자. 멀티 쓰레드를 지원하는 OS Vs. 지원하지 않는 OS

    - 또한 쓰레드를 지원하는 OS Vs. 지원하지 않는 OS가 있음.

      쓰레드를 지원하지 않는다고해서 구시대 유물이 아님에 주의.

    - 여기서 "쓰레드 모델을 지원한다."의 의미는 

      "(커널 레벨에서) 쓰레드 모델을 지원한다."의 의미임에 주의.

      커널 레벨에서 쓰레드 모델을 지원하면 커널 레벨 쓰레드라고 칭함.

      따라서 쓰레드 모델을 지원하지 않는다의 의미는 

      커널 레벨에서 지원하지 않는다는 의미. 만들려면 만들순 있다는 것.

      왜냐면 커널도 결국엔 개발자가 만든 것이기 때문.

      커널도 뭐 거창한 게 아님을 알고있어야 함. 그냥 프로그램.

      OS도 하드웨어에 종속적인 "프로그램", 스케줄러도 사실은 하나의 "프로세스"

    - 그래서 커널 레벨에서 지원하지 않는 모델은 개발자들이 라이브러리를 만듦.

      즉, 니네가 지원안해주니까 내가 프로그램 만들어서 쓰겠다.라는 의미.

      그래서 이런 경우를 유저 레벨 쓰레드라고 칭함. 큰 차이점은 결국 유저가 만드냐 아니냐.

 

  Note) OS(~=커널)만 순수하게 설치된 상태라고 가정해 보자.

    여기에 우리는 필요한 라이브러리들을 설치해나감. (ex. ANSI 표준 Lib, DirectX3d lib, ...)

    여기에는 주변 친구들이 잘 만들어 둔 라이브러리들도 포함이 됨.

    자 이제, 경계를 그음. OS만 순수하게 설치된 상태를 "커널 영역"

    ansistd.lib, dx3d.lib, mySound.lib, ...을 "유저 영역"이라고 경계를 그음.

    즉, 총 메모리 4GB에서 커널 영역이 2GB, 유저 영역이 2GB 이런식임.

    이때 커널 레벨 쓰레드를 지원해주지 않는다면, 유저 영역에는

    유저 레벨 쓰레드 관련 라이브러리가 추가됨.

 

  Note) 아래 그림은 커널 레벨 쓰레드 관련 그림임.

    - 쓰레드를 커널 레벨에서 직접 관리해 주기 때문에 각 쓰레드의 정보들이 

      커널 영역에 존재하는 모습을  볼 수 있음.

    - 즉, 스케줄러가 "쓰레드 단위"로 스케줄링을 해줌.

    - 스케줄링, 즉 관리를 해준다는 건 두 가지를 내포하고 있음.

      1. 해당 쓰레드에 대한 정보를 가지고 있다. 즉 존재 자체를 알고 있음.

      2. 스케줄러가 쓰레드 단위로 상태 정보를 주관함.

        즉, "쓰레드의" 우선순위, "쓰레드의" 상태, ... 로 관리.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

  Note) 아래 그림은 유저 레벨 쓰레드 관련 그림.

    - 쓰레드는 유저 레벨에서 라이브러리 형태로 만들어서 쓰기때문에,

      쓰레드에 관한 정보는 유저 레벨의 해당 프로세스 안에 존재하게 됨.

    - 그래서 스케줄러는 쓰레드의 존재는 모르고, 실행해 봤더니 쓰레드인 상황.

      그렇기에 쓰레드 단위로 스케줄링하는게 아닌, 프로세스 단위로 스케줄링 함.

      즉, "프로세스의" 우선순위, "프로세스의" 상태, ... 이런식.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

  

  Note) 언뜻 보았을땐 큰 차이가 없어보이나, 아주 큰 차이가 있음.

    - 커널레벨 쓰레드는 동일우선순위 기준, 쓰레드 별로 각각 실행 시간을 나눠서 실행되는 형태.

      그럼 쓰레드 A, B, C가 1/3씩 시간을 나눠가짐.

    - 유저레벨 쓰레드는 동일 우선순위 기준, 프로세스 별로 실행 시간을 나눠서 실행되는 형태.

      그럼 프로세스 별로 할당 받은 실행 시간 내에서 다시 쓰레드끼리 실행시간을 나눠 실행.

      즉, 프로세스 A, B가 1/2씩 시간을 나눠가지고 쓰레드 A, B는 1/2의 시간 내에서 나눠가지기에

      1/4씩 시간을 나눠 가지게 됨. 쓰레드 C는 1/2의 모든 시간을 씀.

    - 커널 레벨 쓰레드에서 쓰레드 A가 I/O 상태로 빠졌다고 가정해 보자.

      그럼 쓰레드 B와 쓰레드 C가 1/2씩 시간을 가져가게 됨.

    - 유저 레벨 쓰레드에서는 쓰레드 A가 I/O 상태로 빠졌다고 가정해 보자.

      그럼 스케줄러는 쓰레드 B에게 실행 시간을 줄까?

      아님. 다시 말하지만, 쓰레드 B의 존재자체를 모름.

      그럼 프로세스 단위로 스케줄링 되기때문에 프로세스 A가 Blocked 상태로 빠짐.

      졸지에 쓰레드 B도 같이 딸려감. 즉 쓰레드 C가 모든 시간을 가져가는 셈.

    - 단순하게 여기서 "아, 그럼 유저레벨 쓰레드는 안좋구나!"하면안됨.

      유저가 바로 위에서 언급한 내용을 잘 반영한(고려한) 코드를 작성한다는 가정하에,

      즉, 다른 쓰레드들이 함께 Blocked 상태로 빠져도 상관없는 코드를 작성했다면

      유저 레벨 쓰레드가 결론적으로 더 빠름. 왜 그럴까? 다음 절에서 배움.

 

11.2 쓰레드 구현 모델에 따른 구분

 

11.2-1 커널 모드와 유저 모드

  Note) 우리가 사용하는 모든 프로그램은

    결국 OS의 동작이 필요한 부분과 유저가 작성한 Application 부분으로 나뉨.

    임베디드 프로그래밍에서는 둘을 나누지 않고 그냥 "프로그램"으로 취급.

    그래서 다 같이 Compile해서 실행하는 걸 볼 수 있음. 즉, OS도 그냥 프로그램이라는 관점.

    근데 이런 관점에 매몰되어서, "OS의 동작이 필요한 부분 == Application 부분"으로

    구분하지 않고 동일시 해버리면 문제가 생길 수 있음. 왜그러냐면

    프로그램 실행 -> 프로세스 생성 -> 프로세스만의 메모리 공간(32비트 시스템 기준 4G) 할당됨.

    이 메모리 공간은 선형적인 공간이라, 따로 구분하지 않으면 그냥 모두 접근 가능함.

    그래서 OS의 실행에 필요한 내용도 있고, 유저가 만든 Application 부분이 실행될때 필요한 내용도 있음.

    근데 유저 파트에서 실행 중에 0x101의 내용을 읽어와서 수정했어야 하는데,

    모르고 0x102의 내용을 수정해버림. 근데 마침 딱 그자리에 OS 관련 코드가 있었음.

    즉, OS의 실행에 필요한 내용을 접근할 수 있으니, 접근해서 실수로 수정해 버리면? 

    수정한 부분이 OS의 실행에 있어서 치명적인 부분이라면 큰일 날 수 있음.

    그래서 둘을 나눠놓고자 함. 유저 영역/유저 모드와 커널 영역/커널 모드로 나눠서,

    유저가 만든 내용을 실행 중일때는 유저 모드로 유저 영역의 내용을 접근하며 실행함.

    OS의 실행에 필요한 내용에 접근 중일때는 커널 모드로 커널 영역의 내용을 접근하며 실행함.

    그래서 유저 모드로 실행 중에는 커널 영역에 접근하여 수정하는걸 원천 차단함.

    즉, 디자인마다 다르긴 하지만 뚝 잘라서 2G는 커널 영역, 즉 OS 실행에 필요한 부분

    2G는 유저영역으로 나뉨. 

 

  Note) 이때 주의할 점은, 커널 영역이든 유저 영역이든, 그 용도가

    코드만을 저장하기 위한 공간이 아님에 주의. 실행을 위한 공간임.

    hello world 예제의 경우 코드량은 얼마 되지 않음. 인터넷 게임 정도되면 좀 많아질순 있음.

    그래도 코드만을 위한 공간이 아님. 마찬가지로 OS의 코드만 복사되는 공간이 아님.

    예를 들어서 프로세스 4개가 동시에 생성된다고 가정 해 보자. 

    그럼 커널영역 2GB + 유저영역 2GB 한 묶음이
    4개가 생성된다는건데, OS 코드는 4개 영역에 모두 복사되어서 4개의 OS 코드가 생길까?
    아님 코드는 1개이고 각 메모리 공간 중 커널 영역 일부분에 주소가 매핑될 뿐.

    그래도 코드의 주소가 매핑되어야 F/D/E 할 수 있음은 당연한 이야기.
    다시 이야기 하자면, 어찌되었든 실행을 위한 공간이지 코드로 꽉 채워버리는 공간이 아님

    (강의 중, "OS의 코드가 1번에도 일부 매핑되고 2번에도 일부 매핑되고, ..."라고 말씀하심.)
    (근데 "일부"라는 말이 OS 코드의 "일부"가 아니라 커널 영역 메모리 공간의 "일부"인듯..?)

    그래서 OS 실행 관련 코드는 커널 영역의 일부에 주소가 매핑이 되어 커널 모드로 실행되어지고

    유저 관련 코드는 유저 영역의 일부에 주소가 매핑되어 유저모드로 실행되어짐. 각각 2GB를 

    다 쓰든 못쓰든 우리는 상관하지 않음.

 

  Note) 결론적으로 우리가 사용하는 Windows라는 OS는 

    한 순간에는 하나의 모드로만 동작하게 됨.

    커널 모드 Vs. 유저 모드 중 하나의 모드로만 동작.

    이도저도 아닌 반반상태는 없음. 그래야 메모리 접근을 확실하게 막거나 허용할 수 있기 때문.

    즉, 프로그램을 실행하면 두 모드 사이를 엄청 빠른 속도로 왔다 갔다 함.

    물론 커널 모드 한번 유저모드 한번 커널 모드 한번 유저모드 한번 이런식으로 

    사이좋게 Toggle되진 않음. 커널 모드로 동작할 때는 대표적으로 스케줄러가 동작할 때임.

    즉, 프로세스가 생성될때와 같이 뭔가 커널이 동작해야 할 때라면 커널 모드로 동작하게 됨.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 그래서 커널 레벨 쓰레드는 왜 느릴까?

    관리 대상이 쓰레드임. 즉, 쓰레드 생성될 때마다, 쓰레드끼리의 CS 발생때마다

    커널 모드로 모드 변경이 필요해지는 것.

    그러나 유저 레벨 쓰레드는 쓰레드가 생성되도 유저 모드,

    같은 프로세스 내의 쓰레드 끼리의 CS가 발생되도 커널 모드로 넘어가지 않고

    유저모드이기때문에 속도가 비교적 빠를 수 밖에 없음.

 

  Note) 그래도 커널 레벨 쓰레드의 경우, 커널 모드에서 동작하기 때문에

    접근 불가능한 영역 없이 자유로워서 좀 더 다양한 기능을 제공할 수도 있음.

    

11.2-2 장점과 단점

  Note) 커널 레벨 쓰레드의 장점 및 단점

    - 장점: 커널에서 직접 제공해 주기 때문에, 안정성과 다양한 기능성 제공

    - 단점: 유저 모드에서 커널 모드로의 전환이 빈번

 

  Note) 유저 레벨 쓰레드의 장점 및 단점

    - 장점: 유저 모드에서 커널 모드로의 전환이 필요 없음.

    - 단점: 프로세스 단위 블로킹

 

 

 

댓글