What is Thread?
코딩을 하다 보면 언어가 멀티 태스킹 (multi-tasking), 멀티 스레드 (multi-thread)를 지원한다는 얘기를 들어봤을 거다. AI 언어로 인기를 받는 파이썬도 AI 분야에서 이루어지는 많은 계산을 빨리 하기 위해서 병렬을 가능케 하는 Multi-Tasking과 Multi-Threading을 지원한다. 운영체제 내부에서 응용 프로그램이 작동할 때 응용 프로그램을 프로세스(Process)라 부른다. Multi-Tasking은 운영체제가 여러 process를 동시에 실행하게 하고, Multi-Threading은 process안에서 여러 개의 thread를 동시에 실행한다. Thread는 일하는 노동자 수라고 보면 된다. 따라서 많을수록 동시에 다양한 일을 빨리 많이 할 수 있다.
Process and Thread
Windows 운영 체제에서 process는 Unix-like 운영 체제와 다르게 메모리 영역이 할당된 정적인 (static) 개념이다. Unix-like 운영체제 (example Linux)는 process에 CPU 시간을 할당하는 동적(dynamic) 개념이다. Windows 운영체제에서 각 Process는 운영 체제가 Virtual Memory Address를 할당하여 위치를 기억하며, 그 안에는 사용자 / 커널 영역으로 나뉜다. 커널 영역은 Windows 운영체제가 직접 관리한다. 사용자 영역 안에는 1. code, resource, data 2. heap memory 3. environment variable(환경 변수) 4. threads 이 있으며, 커널 영역 안에는 사용자 영역과 소통하는 process 커널 객체와 file, socket.. 등이 있다.
CPU 시간을 할당받아 process를 실행하려면 thread가 최소 한 개는 있어야 한다. 최초로 생성되는 thread를 Primary Thread(Main Thread)라 부른다. Thread은 process의 Virtual Memory Address에서 흐름을 담당하며, 운영 체제는 CPU 시간을 Thread에 나눠서 할당하여 병렬적으로(parallel)하게 thread가 동시에 실행하게 한다.
Thread 구성 요소
Stack: 함수 인자, 지역 변수를 저장하는 메모리
TLS(Thread Local Storage) : 각 thread의 고유 데이터 메모리
Thread Kernel Object (스레드 커널 객체)
Thread는 CPU 시간을 할당하여 사용하는 동적 개념이며, 자신만의 고유한 CPU register 값을 갖는다. 자신만의 스택 공간이 있다. 컴퓨터를 사용하면서, 인터넷 창에서 유튜브를 보다가 구글링을 해도 음성이 끊기지 않음을 볼 수 있다. 그때 Thread는 각 작업을 번갈아가며 수행하여 동시에 수행되는 것처럼 보이게 한다. 이것을 Thread Kernel Object가 관리며, threa의 상태 저장과 복원은 모두 여기서 실행된다.
Process에 여러 개의 Thread가 할당 되면, 모든 thread는 process의 Virtual Memory Address에 존재하는 code, resource, data (전역 변수, 정적 변수), heap, environmental variable과 운영체제가 process를 위해 할당한 각종 자원을 공유한다.
CPU Scheduling
시간을 효율적으로 분배하기 위해 스케줄을 짜는 것처럼, 한정된 CPU 자원을 효율적으로 분배하기 위해 CPU scheduling을 짠다. Unix-like 운영체제에서는 여러 개의 process에, Windows 운영체제에서는 여러 개의 thread에 분배한다. Windows 운영체제는 우선순위에 기반하여 Scheduling 한다. 이 우선순위는 우선순위 클래스(class) / 우선순위 레벨(level)로 결정된다.
우선순위 클래스 : process 속성. 같은 process 안에 있는 thread는 동일한 클래스를 갖는다
우선순위 레벨 : thread 속성. 같은 process에 속한 thread 간 상대적인 우선순위를 결정한다.
+ MFC Thread
하단은 Windows 기반 라이브러리인 MFC에서 사용되는 thread에 해당하는 설명이다. 두 종류로 나뉜다.
1. 작업자 thread 2. 사용자 인터페이스 thread. 1번은 메시지 loop이 없고, 2번은 메시지 loop이 있다.
작업자 Thread
AfxBeginThread() 함수로 가능하다. AfxBeginThread() 함수는 CWinThread 타입의 Thread Object를 동적 생성하고 내부적으로는 thread를 생성 후, thread object의 주소를 반환한다. 이 반환 값을 이용하여 CWinThread class의 member function을 호출하면 thread를 제어할 수 있다.
작업자 thread를 종료할 때는, AfxEndThread() 함수를 호출하거나, thread 제어 함수가 종료 코드를 리턴하도록 한다.
UI Thread
작업자 thread와 달리 message loop이 있어서 사용자와 상호작용이 가능하다. Event 처리가 가능하다. UI Thread의 대표적인 예는 MFC 응용 프로그램에서 기본으로 생성되는 응용 프로그램 (process) 객체다. 따라서 MFC 응용 프로그램은 최소 1개의 UI thread가 존재한다. 방법은 아래와 같다.
1. CWinThread를 상속하여 새로운 class를 만든다 2. class 선언부와 구현부에 각각 DECLARE_DYNCREATE, IMPLEMENT_DYNCREATE 매크로를 선언한다. 이 매크로를 선언함으로써 RTCI(Run Time Class Information)과 Dynamic Object Creation을 지원한다. 3. CWinThread 클래스가 제공하는 가상 함수 중 일부를 override 한다. 그중 InitInstance()는 반드시 재정의해야 한다. 4. AfxBeginThread() 함수를 이용해 새로운 UI thread를 생성한다.
Thread Synchoronization 스레드 동기화
Thread를 여러 개 생성할 수 있다. 만약 thread들이 동일한 data를 공유한다면, data 조작이 원하는 방식으로 이루어지지 않을 수 있다. 같은 data를 공유할 때는 한 thread가 작업을 완료한 후에, 대기 중인 다른 thread에 알려주어야 한다. Windows 운영체제에서는 이를 관리하는 매개체를 동기화 객체 (Synchronization Object)라고 부르며, 임계 영역, 뮤텍스, 세마포어가 있다.
임계 영역 Critical Section
공유 data를 다루는 여러 thread가 있을 때 한 개의 thread만 접근 가능하다. 하단에 Mutex, Event, Semaphore와 다른 점은 Kernel Mode가 아닌 Client Mode로 동작하여 속도가 빠르지만 다른 process에 속한 thread와의 동기화는 불가능하다.
thread.Lock()을 걸면 공유 data에 접근 가능하고, thread.Unlock()을 호출하기 전까지 다른 thread는 대기한다.
임계 영역 사용 전:
임계 영역 사용 후:
하단에 방식들도 기본 원리는 위와 동일하다.
뮤텍스 Mutex (Mutual Extension)
기능은 임계 영역과 동일하다. 공유 자원에 접근하는 thread에 대해서 하나만 접근 가능하게 하지만, Client Mode가 아닌 Kernel Mode로 동작하여 상대적으로 속도는 느리다. 하지만 다른 process에 속한 thread에 대해서 동기화가 가능하다. Critical Section과 방식은 동일하다. Lock()을 통해 공유 자원에 단독으로 접근 가능하고, UnLock()을 통해 대기 중인 다른 thread가 접근 가능하게 한다. 뮤텍스의 특징 은 Lock() 함수를 호출한 thread가 해당 Mutex object를 소유하여 UnLock() 함수는 그 thread만이 호출 가능하다.
Mutex 사용 전:
Mutex 사용 후:
이벤트 Event
Signaled, NonSignaled 두 개의 상태를 가진 동기화 Object이다. Thread가 작업을 완료한 후 대기중인 다른 thread에 알려줄 때 사용한다.
절차 : 1. Event를 Nonsignaled 상태로 생성 2. thread가 작업을 생성하고, 나머지 thread는 event에 대해 Lock()을 호출하여 event가 signaled 상태가 될 때까지 대기 3. thread가 작업을 완료하고 event를 signaled 상태로 전환 4. 대기 중인 thread 중 하나 혹은 전부가 Lock()이 풀린다.
세마포어 Semaphore
Data에 접근할 수 있도록 thread 수를 제어하는 동기화 object이다. 다른 동기화 object와 달리 Semaphore는 사용할 수 있는 자원(thread)의 개수를 세는 Resource Count를 유지하여, 동시에 실행되는 thread 수를 조절할 수 있다.
절차: 1. Semaphore 생성. Resource Count를 사용 가능한 자원의 개수로 초기화 2. 자원을 사용할 thread는 자신이 필요한 resource의 개수만큼 Lock()을 호출하고, 호출에 성공할 때마다 Resource Count 값은 1씩 감소한다. 3. 자원 사용을 마치면 자신이 사용한 resource의 개수만큼 UnLock()을 호출하고, 호출에 성공할 때마다 Resource Count 값은 1씩 증가한다.
'Computer Science > C++' 카테고리의 다른 글
CMake Shared Lib Linker (0) | 2024.02.26 |
---|---|
알기 쉬운 CMake 개념 정리 및 구조 파악. CMake 튜토리얼 (Tutorial) (1) | 2023.12.22 |
VSCode에서 C++ include 설정하기. include error. MSYS64, UCRT, Mingw (0) | 2023.08.29 |
Multithreading Mutex using MSYS2, Ucrt64 (Mingw64) 멀티스레드 (Windows 운영체제, C++) (0) | 2023.08.23 |