🧵 주제

C++에서 std::thread를 이용한 스레드 생성, 관리, 동기화 기초 실습 정리

하나의 프로세스에서 여러 개의 스레드를 생성하고, 병렬로 작업을 처리하며, join, detach, joinable, hardware_concurrency 등 핵심 API 사용법과 그 의미를 실습 코드와 함께 학습한 내용을 완전하게 정리한 글이다.


📘 개념

✅ 스레드란?

  • 스레드(Thread)는 프로그램 내에서 실행되는 가장 작은 단위이며, 하나의 프로세스 안에서 여러 개를 생성할 수 있다.
  • C++11부터 도입된 std::thread를 통해 OS에 상관없이 동일한 방식으로 스레드를 생성하고 실행할 수 있다.
  • 각 스레드는 독립적으로 실행되며, 병렬 처리, 즉 동시에 여러 작업을 수행할 수 있도록 해준다.

✅ 스레드의 생명 주기

  1. 생성: std::thread 생성자에 실행할 함수와 인자를 넘겨주면 바로 새로운 스레드가 시작된다.
  2. 대기: 스레드는 join()으로 명시적으로 종료될 때까지 기다리거나, 또는 detach()로 분리해서 백그라운드에서 실행시킬 수 있다.
  3. 종료 처리 필수: 만약 join() 또는 detach()를 하지 않고 std::thread 객체가 소멸되면 std::terminate()가 호출된다.

✅ 스레드 함수 인자 전달

  • std::thread는 내부적으로 가변 템플릿 생성자를 사용하므로, 인자 개수와 타입이 다양해도 함수와 함께 넘기기만 하면 자동으로 처리된다.
  • 예: std::thread(HelloThread2, 10);

✅ 병렬 실행과 동기화 이슈

  • 여러 스레드가 하나의 전역 변수(sum 등)를 동시에 접근하면 예기치 못한 결과가 발생할 수 있다.
  • 이를 경쟁 상태(Race Condition)라 하며, 동기화 도구(mutex, atomic 등)를 사용하지 않으면 데이터 손상 위험이 있다.

📚 용어 정리

용어설명
std::threadC++ 표준 스레드 클래스. 스레드 생성 및 제어를 담당
join()현재 스레드가 대상 스레드가 종료될 때까지 대기
detach()스레드를 백그라운드에서 실행하도록 분리 (제어권 상실)
joinable()해당 스레드 객체가 조인 가능한 상태인지 확인
get_id()해당 스레드 고유 ID 반환
hardware_concurrency()사용 가능한 논리 CPU 코어 수 반환
가변 템플릿 생성자함수 인자 개수와 타입에 맞게 유연하게 처리하는 생성자 구조

💻 코드 분석

✅ 예제 1: 단일 쓰레드 생성 및 join

std::thread t;

auto id1 = t.get_id(); // 아직 실행할 함수가 없으므로 id는 0

t = std::thread(HelloThread); // HelloThread 함수 실행하는 쓰레드 생성

int32 count = t.hardware_concurrency(); // 시스템의 논리 코어 수 반환

auto id2 = t.get_id(); // 실제 실행되는 쓰레드 ID

if (t.joinable()) 
    t.join(); // 스레드 종료까지 대기

cout << "Hello Main" << endl;

📌 std::thread 객체는 함수 실행과 동시에 스레드를 시작한다. join()을 호출하지 않으면 main()이 먼저 끝날 수 있어 위험하다.


✅ 예제 2: 여러 개의 쓰레드를 벡터로 관리

std::vector<std::thread> threadVec;

for (int32 i = 0; i < 10; ++i)
{
    threadVec.push_back(std::thread(HelloThread2, i)); // i값 전달하여 10개 스레드 생성
}

for (int32 i = 0; i < 10; ++i)
{
    if (threadVec[i].joinable())
        threadVec[i].join(); // 각각의 스레드 종료까지 기다림
}

cout << "Main Finished!" << endl;

📌 스레드 객체를 컨테이너에 저장하여 여러 개를 생성하고 관리할 수 있다. 실행 순서는 OS 스케줄러에 따라 랜덤하므로 출력 순서도 비결정적이다.


✅ 예제 3: 공유 자원 충돌 예시

int32 sum = 0;

void Add()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		int32 eax = sum;
		eax = eax + 1;
		sum = eax;
	}
}

void Sub()
{
	for (int32 i = 0; i < 100'0000; i++)
	{
		int32 eax = sum;
		eax = eax - 1;
		sum = eax;
	}
}

int main()
{
	Add(); Sub();
	cout << sum << endl; // 항상 0

	std::thread t1(Add);
	std::thread t2(Sub);

	t1.join(); t2.join();
	cout << sum << endl; // 실행할 때마다 결과가 달라짐!
}

📌 Add()Sub()을 각각 순차적으로 호출하면 결과는 항상 0이지만, 스레드로 병렬 실행하면 결과가 매번 달라진다. 이건 전형적인 경쟁 조건(Race Condition) 문제다.


🧠 핵심

  • std::thread는 C++에서 멀티스레딩을 구현할 수 있는 핵심 클래스이며, 함수와 인자를 넘기면 실행 즉시 스레드가 시작된다.
  • 스레드를 생성했다면 반드시 join() 또는 detach()를 호출해 종료를 명시적으로 처리해야 한다. 안 하면 std::terminate()가 호출된다.
  • 여러 개의 스레드를 사용할 땐 vector<thread> 등 컨테이너로 관리하면 편리하다.
  • hardware_concurrency()는 시스템의 코어 수를 반환하므로, 쓰레드 풀 구성이나 스레드 수 조절 시 유용하다.
  • 공유 자원에 여러 스레드가 접근하면 경쟁 조건이 발생하며, 예기치 않은 결과가 나온다. 이를 방지하려면 mutex, atomic 등의 동기화 장치를 사용해야 한다.
  • std::thread의 생성자는 가변 템플릿으로 구성되어 있어 인자 전달을 자동으로 처리한다.

profile
李家네_공부방

0개의 댓글