C++ 개념 정리

22. Thread

CE : 하랑 2026. 1. 31. 17:24

 


프로그램(Program) 이란

- 어떤 작업을 위해 실행할 수 있는 파일

 

프로세스(Process) 란

- 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램

 

- 메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)
- 운영체제로부터 시스템 자원을 할당받는 작업의 단위

 

 


스레드(Thread)란

- 프로세스 내에서 실행되는 여러 흐름의 단위

 

3. Thread -> 쓰레드 로드

(1) 쓰레드란 뭔가요? 

- 스택은 실행메모리를 담고 실행 흐름(함수)이라고 한다.

-> 이 실행 흐름을 쓰레드라고 합니다.

 

 

(2) 쓰레드의 개수는 몇개가 적당하다고 생각한가요? -> 경험

- 암달의 법칙에 의거한 CPU 코어 개수에 비례 CPU 코어의 개수 *2 +1, CPU 코어의 개수 * 2 가 적당하다고 생각합니다.

- 다이렉트에서는 코어의 개수만큼 만듬

 

(3) 10개의 일을 1000개의 쓰레드가 처리하면 더 빠를까요?

1개의 일에 대한 일의 처리량과 일 분배에 따라 다를거 같다. 예를들어 간단한 작업이 10개면 1000개의 쓰레드를 미리 만들어두면 일하는 쓰레드 따로 있고 아무것도 하지 않는 쓰레드도 존재할 것이기에 쓰레드를 만드는 비용까지 생각하면 낭비가 심하고 더 느리다고 생각합니다. 

 

(4) 쓰레드를 쓰는 것만으로도 빨라질가요? 아니다. 대표적인 예시 (단일 쓰레드)

단일 쓰레드나 처리하는 일의 양에 비해 적은 수의 쓰레드 면 더 느리다 이유?

 

메인 쓰레드로만 처리하면 본래 일의 크기 작업 시간이 걸리지만 , 적은수 즉 단일 쓰레드로 하면, 쓰레드 만드는 비용, 안전하게 일 주고 받기등 의 처리들이 더 추가 되기에 본래 실행하는 시간보다 더 느리다.

 

 

(5) 사용할 쓰레드를 미리 만들어두고 사용하는 이유 (쓰레드 풀)? -> 메모리 풀?(검색해보기)

런타임 맨앞에 미리 쓰레드를 많이 만들고 다시는 안만든다는 것 (init)

한번 만든 쓰레드를 재활용해서 사용하겠다는 것입니다.

 

 

(6) 쓰레드가 공유하는 메모리 범위

쓰레드는 스택은 각자 메모리를 가지지만 힙, 데이터, 코드는 공유한다.

-> 크리티컬 섹션이라고 부르는 임계영역은 각 쓰레드가 공유하는 메모리에서 발생한다.

 

 

(7) 쓰레드를 많이 만드면 좋은가 >?

- 많이 만드면 (비지 쓰레드) -> 양이 많아 지면 많아질수록 CPU 점유율 증가 -> 윈도우가 멈출 가능성 

 

쓰레드 = 함수
비지 쓰레드 = 함수안의 반복문

 


 

8) 임계영역 , 크리티컬 섹션 -> 문제 어케 해결함?

- std::atomic

-> 아토믹은 언제 사용하느냐 16바이트 이하의 아주 작은 메모리 영역의 쓰레드 동시접근을

빠르게 막고 싶을때 다른 쓰레드에 안전하게 접근할수 있게 해주는 기능들을 보통 락이라고 하는데

락중 빠른 계열에 속합니다.

 

- > Window : 인터락

- > C++ std : 아토믹 => 내부에서 window 인터락을 쓰고 있다

 

-> 아토믹을 사용하니 잘 중단점이 걸리는 것을 알수 있고 쓰레드에서 안전하게 메모리를 보호하는 기능들중 가장 빠르지만

-> 단점이 하나 있는데 보호할수 있는 메모리의 크기가 16바이트 -> 맵같이 크고 복잡한 자료구조는 보호가 불가능합니다.

 

- std::mutex

->  그래서 자료구조나 특정 영역을 보호할때는 mutex를 지원해준다.

-> std:;lock_guard와 std::lock의 차이점은?

std::lock_guard std::lock은 모두 C++에서 다중 스레드 환경에서의 동시성 문제를 해결하기 위한 기술 중 하나인 뮤텍스(mutex)와 관련된 클래스와 함수이다. 하지만 각각의 역할 및 사용 방법이 다르기 때문에 차이가 있다.

 

(9) 컨텍스트 스위칭과 CPU 연관성

- 쓰레드가 일을 하기 위해서 뭔가 준비를 하는 작업을 컨텍스트 스위칭이라고 합니다

- CPU에 실행할 프로세스를 교체


- 컨텍스트 스위칭이 일어나지 않은 쓰레드를 busy쓰레드라고 합니다.

 

컨텍스트 스위칭이 너무 자주 일어나도 일어나지 않아도 둘다 문제

 

컨텍스트 스위칭이 자주 일어나면 적은 작업량에 준비하는 작업들만 많이 하는 것으로 오히려 속도면에서 더 느릴 수 있다.

 

컨텍스트 스위칭이 잘 일어나지 않으면 그만큼 CPU를 계속 점유하고있기에(비지 쓰레드 처럼 동작) 이런 작업들이 많으면 많을수록 중간에 정지하는 것처럼 움직인다.

 

모든 적당한 일 분배, 적당한 작업량 등 -> 판단은 경험

 

 

-> 일분배 (IOCP) -> IOCP 공부하기  -> 아직 안끝남

적당한 수의 쓰레드를 이용해 일을 균등하게 분배해서 일을 시키는 것이 제일 베스트

 


 

쓰레드 추가 공부

!참고

: 쓰레드의 리턴값은 없다쓰레드는 리턴값이 없기 때문에 어떠한 결과를 반환하고 싶다면 포인터의 형태로 전달하면 된다.

 

JOIN

(1) 쓰레드는 언제 처리될지 모름

#include <string>
#include <vector>
#include <iostream>
#include <list>
#include <thread>
#include <functional>

void test1()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 1 실행중" << "\n";
	}
}

void test2()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 2 실행중" << "\n";
	}
}

void test3()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 3 실행중" << "\n";
	}
}

int main()
{
	std::thread th1,th2,th3;

	// 서로 언제 처리될지 모름
	th1 = std::thread(std::bind(test1));
	th2 = std::thread(std::bind(test2));
	th3 = std::thread(std::bind(test3));
	
	// join 해당하는 쓰레드들이 실행을 종료하면 리턴하는 함수
	th1.join();
	th2.join();
	th3.join();
}

 

 

-> join을 안해주면 터짐

 

이유?

쓰레드들의 내용이 실행되기 전에 main 함수가 종료되었기 때문이다.

-> 앞서 말한 쓰레드는 언제 실행되고 언제 종료 될지 모르기 때문에 확실한 작업 종료의 리턴을 확인을 못하고 했을 때 main 함수가 먼저 종료되어버려 터짐

 

C++ 표준에 따르면, join 되거나 detach 되지 않은 쓰레드들의 소멸자가 호출된다면 예외를 발생시키도록 명시되어 있다. 따라서, 쓰레드 객체들이 join이나 detach모두 되지 않았으므로 위와 같은 문제가 발생하게 된다.

 

 

detach

detach란 말 그대로, 해당 쓰레드들을 실행시킨 후, 잊어버리는 것이라 생각하면 된다. 대신 쓰레드는 백그라운드에서 돌아가게 된다.

#include <string>
#include <vector>
#include <iostream>
#include <list>
#include <thread>
#include <functional>

void test1()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 1 실행중" << "\n";
	}
}

void test2()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 2 실행중" << "\n";
	}
}

void test3()
{
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << "쓰레드 3 실행중" << "\n";
	}
}

int main()
{
	std::thread th1,th2,th3;

	// 서로 언제 처리될지 모름
	th1 = std::thread(std::bind(test1));
	th2 = std::thread(std::bind(test2));
	th3 = std::thread(std::bind(test3));
	
	// detach -> 해당 쓰레드들을 실행시킨 후, 잊어버리는 것
	th1.detach();
	th2.detach();
	th3.detach();

	std::cout << "main 함수 종료" << "\n";
}

기본적으로 프로세스가 종료될 때, 해당 프로세스 안에 있는 모든 쓰레드들은 종료 여부와 상관없이 자동으로 종료된다.
즉 main 함수에서 메인 함수 종료!를 출력하고, 프로세스가 종료하게 되면, func1, func2, func3 모두 더 이상 쓰레드 작동중!을 출력할 수 없게 된다.
 
쓰레드들을 detach 했기 때문에 main함수에서 다른 쓰레드들이 종료될 때까지 기다리지 않은 모습을 확인할 수 있다.
위 부분이 그냥 쭈르륵 실행되어서 쓰레드들이 채 문자열을 표시하기 전에 프로세스가 종료된 것이다.

 

 

std::thread::joinable()

쓰레드가 실행 중인 활성 쓰레드인지 아닌지 확인합니다.

 

 

std::atomic -> 리소스 로딩 할때 사용 -> 참고 해서 공부해보기

-> 

int NormalN = 100;

 

(1)  작업이 겹치는 순간이 발생 -> 임계영역, 크리티컬 섹션  (공유하는 메모리에 작업 겹침)

 

std::atomic_int atomicN = 100;

 

-> std::atomic 겹치는 메모리 공간 작업 통제 

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>


// 전역 변수 -> 데이터 영역
int NormalN = 100;
std::atomic_int atomicN = 100;

void NormalAdd(const int n)
{
    for (size_t i = 0; i < 100; i++)
    {
        --NormalN;
        std::cout <<n<<" 번 쓰레드 " << NormalN << "\n";

        if (NormalN == 0)
        {
            break;
        }
    }
    
}

void NormalAtomic(const int n)
{
    for (size_t i = 0; i < 100; i++)
    {
        --atomicN;
        std::cout << n << " 번 쓰레드 " << atomicN << "\n";

        if (atomicN == 0)
        {
            break;
        }
    }

}

int main() 
{
    std::thread th1, th2,th3;

   /* th1 = std::thread(NormalAdd,1);
    th2 = std::thread(NormalAdd,2);
    th3 = std::thread(NormalAdd,3);

    if (th1.joinable())
    {
        th1.join();
    
    }

    if (th2.joinable())
    {
        th2.join();

    }

    if (th3.joinable())
    {
        th3.join();

    }*/

    th1 = std::thread(NormalAtomic, 1);
    th2 = std::thread(NormalAtomic, 2);
    th3 = std::thread(NormalAtomic, 3);

    if (th1.joinable())
    {
        th1.join();

    }

    if (th2.joinable())
    {
        th2.join();

    }

    if (th3.joinable())
    {
        th3.join();

    }
    
}

 

 

std::mutex -> 쓰레드를 통해서 안전하게 자료구조 관리

(1) mutex 보호 없이 사용하는 경우

// 전역 변수 -> 데이터 영역
std::vector<int> num;
//std::mutex num_mutex;

 

std::function<void()> test;

    test = [] {
        for (int i = 0; i < 10; i++)
        {
            //std::lock_guard<std::mutex> Lock(num_mutex);
            num.push_back(i);
        }
    };

   

    th1 = std::thread(test);

    th2 = std::thread(test);

    th3 = std::thread(test);

- 터지거나 잘 실행되는 경우도 있음 -> 언제 어떻게 터질지 모르기에 std::mutex 해주는게 맞음

std::lock_guard는 많이 쓰이는 락 종류로써 다음처럼 객체 생성 시에 lock되며 객체가 소멸시에 unlock 되는 특성을 가지고 있습니다.

 

-> 알아서 Lock 해주며 Unlock 해줌 사용하기 편함

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <mutex>
#include <windows.h> // Sleep();

// 전역 변수 -> 데이터 영역
std::vector<int> num;
std::mutex num_mutex;



int main() 
{
    std::thread th1, th2,th3;

    std::function<void()> test;

    test = [] {
        for (int i = 0; i < 10; i++)
        {
            // 공통된 자료구조에 값을 넣어 줄때 멀티 쓰레드면 동시에 접근하는 경우도 있다.
            // 그래서 동시 접근시 들어온 순서대로 넣어주기 위해
            // lock과 unlock 개념 이용
            // 누군가 들어오면 뒤에 들어오는 데이터는 잠시 대기 
            // 다 들어가고 나서 다음 순번 처리
            // -> mutex 사용해서 자료구조 관리할거면 필수 !
            std::lock_guard<std::mutex> Lock(num_mutex); 
            num.push_back(i);

        }
    };

   

    th1 = std::thread(test);

    th2 = std::thread(test);

    th3 = std::thread(test);

    th1.join();
    th2.join();
    th3.join();
   
    for (int i = 0; i < num.size(); i++)
    {
        std::cout << num[i] << "\n";
        Sleep(1000); //1000ms로 1초를 의미합니다. -> Delay 생각
    }
    
}
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
#include <functional>
#include <mutex>
#include <windows.h> // Sleep();

// 전역 변수 -> 데이터 영역
std::vector<int> num;
std::mutex num_mutex;



int main() 
{
    std::thread th1, th2, th3;

    std::function<void()> test;
    std::function<void()> SleepFun;

    test = [] {
        for (int i = 0; i < 10; i++)
        {
            // 공통된 자료구조에 값을 넣어 줄때 멀티 쓰레드면 동시에 접근하는 경우도 있다.
            // 그래서 동시 접근시 들어온 순서대로 넣어주기 위해
            // lock과 unlock 개념 이용
            // 누군가 들어오면 뒤에 들어오는 데이터는 잠시 대기 
            // 다 들어가고 나서 다음 순번 처리
            // -> mutex 사용해서 자료구조 관리할거면 필수 !
            std::lock_guard<std::mutex> Lock(num_mutex); 
            num.push_back(i);

        }
    };

    SleepFun = [] {
        Sleep(5000);
    };

  
    th1 = std::thread(test);

    th3 = std::thread(SleepFun);

    th2 = std::thread(test);

    if (th1.joinable())
    {
        th1.join();
    
    }


    if (th2.joinable())
    {
        th2.join();

    }

    if (th3.joinable())
    {
        th3.join();

    }
   
    for (int i = 0; i < num.size(); i++)
    {
        std::cout << i << "\n";
        //Sleep(1000); //1000ms로 1초를 의미합니다. -> Delay 생각
    }
}


쓰레드는 언제 작업이 끝날지 모르기때문에 주의 해야한다.  ex) 렌더링 로드 중인데 관련한 Actor를 스폰할 수 있나?

-> DX 쓰레드 로드 확인하고 공부하기

 

- > 추가적 지식

(1) DX기준 쓰레드 동작하는 것

- 라이브러리 추가 한 것들 (FMOD, Imgui, 폰트 등)

- Ntdll 쓰레드 비주얼스튜디오가 내 프로그램을 감시하는 쓰레드이다. -> 비주얼 스튜디오가 감시하기 위한 쓰레드

- 주 쓰레드

 

(2) DX 엔진에서 사용하는 쓰레드 종류

- Std::thread는 내부에서 결국 os가 제공해주는 쓰레드 함수를 사용해서 쓰레드를 만드는 것이다

- Std::thread NewThread 라고 쓰지만 Window에서 사용한다면 내부에서 __beginthreadex  __beginthread 사용하고 있다

 

 

(3) #include <window>

- Window에서 원자성을 지켜주는 기능은 인터락 함수들이라고 부른다.

- Std:: thread를 초반에 std로 제공못해줬던 이유다.  -> 그냥 변수를 쓰는것보다 무조건 더 느리고 무조건더 연산이 많아질수밖에 없습니다.

 

- 쓰레드를 여러 개 쓰면 내부에서 임계영역을 막기 위해서 여러가지 함수나 기능들을 사용해야 한다. 그것때문에 쓰레드로 로딩하는게 더 느려질수도 있다.

Ex)

그냥 로딩 1 => 쓰레드에 안전하지 않으므로 게임자체가 망할수가 있다.

쓰레드에 안전한 로딩 3

 

속력

- 단일 쓰레드는 보통 더 느릴것이다. 특히나 단일쓰레드 로딩은 무조건적인 느리게 만드는 것이다.

- 쓰레드는 일반적으로 순서 상관없이 최대파워로 최대한 일을 나눠서 다수의 쓰레드로 처리해야 진정한 속력의 증가를 볼수가 있다는 것이다.

'C++ 개념 정리' 카테고리의 다른 글

24. 정수  (0) 2026.02.01
23. IOCP  (0) 2026.01.31
21. class  (0) 2026.01.31
20. Serializer(직렬화)  (0) 2026.01.31
19. 유니온  (0) 2026.01.18