C++ Thread

윤형·2026년 1월 3일

C++ Game Server

목록 보기
6/10
post-thumbnail

Thread

원래 C++에서는 윈도우용 리눅스용 스레드를 만드는 방식이 따로 나누어져 있었지만 최근 업데이트 되면서 c++ 자체에서 thread라이브러리를 제공하게 되었다.

#include<thread>

void HelloThread() {
	cout << "HelloThread" << endl;
}

int main() {
	thread t(HelloThread);
    
    cout << "HelloMain" << endl;
    
    t.join();
}
  • thread를 생성하는 방법은 그냥 thread객체를 생성하는 거 처럼 만들면 된다.
  • 그리고 인자로는 그 스레드가 어떤 함수를 처리할지를 넣어주면 된다.
  • join()함수를 사용하면 이 스레드가 작업을 완료하기를 기다리게 되는 것이다. (블로킹)
  • 참고로 Join을 사용하지 않게 되면 스레드 객체가 소멸할 때std::terminate()가 호출이 되어 비정상 종료가 되게 된다.

thread.hardware_concurrency

이 프로그램이 동시에 돌릴 수 있는 스레드의 개수를 알려주는 힌트값을 반환하는 정적 함수이다. 근데 무조건 확률이 100%는 아니여서 약간 힌트용으로 사용한다.

#include<iostream>
#include<thread>
using namespace std;


void HelloThread() {
	cout << "HelloThread" << endl;
}

int main() {
	thread t(HelloThread);
    
    cout << "HelloMain" << endl;
    
    int a = t.hardware_concurrency();
    cout << "Hardware Concurrency: " << a << endl;
    t.join();
}
  • 이렇게 하면 본인 노트북에서는 8이라는 값이 나오게 된다.

thread.get_id

스레드 마다 생성이 되면 각자 id를 가지고 있는데 이 값을 리턴하는 함수이다. 근데 값이 1,2,3 이렇게 순차적으로 오르는 것은 아니고 들쑥날쑥 가지게 된다.

int main() {
	thread t(HelloThread);
    
    cout << "HelloMain" << endl;
    
    int a = t.hardware_concurrency();
    cout << "Hardware Concurrency: " << a << endl;

    auto b = t.get_id();
    cout << "Thread ID: " << b << endl;

    t.join();
}
HelloMain
HelloThread
Hardware Concurrency: 8
Thread ID: 0x16b197000
  • 이렇게 각 스레드마다 중첩되지 않는 독립적인 id를 가지게 된다.

thread.detach

detach는 중요한 개념인데. 이는 메인 스레드에서의 연결을 끊어서 별도의 스레드로 OS 백그라운드에서 동작시키게 하는 것이다. 이렇게 되면 main이 종료가 되어도 새로 생긴 thread는 종료가 되지 않는다.

void HelloThread() {
	cout << "HelloThread" << endl;
}

int main() {
	thread t(HelloThread);
    
    cout << "HelloMain" << endl;
    t.detach();

    this_thread::sleep_for(100ms);
}
  • 이런식으로 동작시키면 된다.
  • 참고로 sleep_for가 없으면 메인 스레드는 detach()를 받으면 바로 리턴되기 때문에 다른 스레드의 결과를 받아보지 못하고 종료가 된다.
  • 중요한거는 detach를 사용하면 별도로 동작한다는 점이다.

thread.joinable

join이 가능한 상태인지를 확인하는 함수이다.

void HelloThread() {
	cout << "HelloThread" << endl;
}

int main() {
	thread t;
    auto id = t.get_id();
    cout << "Thread ID: " << id << endl;
    t = thread(HelloThread);
    if (t.joinable()) {
        t.join();
    }
    else {
        cout << "Not Joinable" << endl;
    }
}
Thread ID: 0x0
HelloThread
  • 이런식으로 조금 더 일반적으로 join이 가능한지를 확인하고 join을 걸게 된다.
  • 그리고 추가적으로 thread가 아직 할당되지 않은(만들어지기만 한 스레드)는 id를 호출해보면 0이 나온다.

lvalue, rvalue

  • lvalue: 메모리 위치를 참조하는 식, 이름 있고 주소(&)가 있는 값.
  • rvalue: 이름 없고 주소 없는 임시값.
int val = 90;
val = val + 10;
  • 이렇게 있을때 val이 가르키는 주소에 10이라는 값을 넣게 된 것이다. 따라서 val은 L-Value가 되는 것이다. (메모리 위치를 참조.)
  • 하지만 밑에 식을 보면 왼쪽의 val은 여전히 L-Value지만 오른쪽에 있는 val + 10은 그 자체로 임시값이 되면서 R-Value가 되는 것이다.
cout << function() << '\n';
  • 이렇게 되어있는 function()이라는 함수도 R-Value가 된다. 왜냐하면 이 식이 끝나면 더이상 참조할 수 없는 임시 값이기 때문이다.
int val = 90;
int& ref = val;
  • 기존 &을 이용하면 L-value 참조자를 만들 수 있었다.
int function () {
	return 1;
}

int&& ref = function();
cout << ref << endl;
ref = 100;
cout << ref << endl;
1
100
  • 이렇게 &&을 이용하면 R-Value에 직접 바인딩 할 수 있다.
  • 참고로 이렇게 하면 1이라는 값이 출력되고 후에 값을 바꾸어 100이라는 값이 출력된다.
  • 핵심은 원래라면 한번 선언되고 사라지는 function()이 ref의 생명주기 만큼 같이 따라가게 된다는 것이다.

thread에 인자 추가

지금까지는 그냥 인자가 없는 단순한 함수였는데, 만약에 인자를 넣어보려면 어떻게 하면 될까?

template <class _Fp, class... _Args, 
__enable_if_t<!is_same<__remove_cvref_t<_Fp>, thread>::value, int>>
thread::thread(_Fp&& __f, _Args&&... __args) {
	_Start(_STD forward<_Fn>(__f), _STD forward<_Args>(_-args))
}
  • thread를 타고 코드를 확인해보면 대충 이런식으로 되어있다.
  • 이걸 조금이라도 이해하기 위해서는 일단 Ivalue와 rvalue가 뭔지 알아야 한다.
  • _FP&&를 받는다. 이러면 특정 함수를 R-Value로 받는다는 의미다.
  • 그뒤에 _Args&&... __args 이렇게 되어있는데, 이는 인자를 여러개를 받을 수 있다는 뜻이다.
  • 핵심은 여러 인자를 뒤에 추가해서 받아서 그걸 실행한다고 생각하면 된다.
void HelloThread(int num) {
	cout << "HelloThread: " << num << endl;
}

int main() {
	thread t;
    t = thread(HelloThread, 1);
    t.join();
}
  • 이렇게 필요한 인자는 스레드 객체에서 동작시킬 함수 뒤에 인자에 순차적으로 넣으면 된다.

Thread Local Storage (TLS)

TLS라고 하는 것은 각 스레드가 독립적인 데이터를 저장하는 것이다.

그런데 각 스레드마다 이미 별도의 스택 공간이 있을텐데 왜 TLS가 따로 있는것일까? 그것은 스택은 함수를 위한 메모리 공간에 가깝고 함수가 정리가 되면서 거기서 사용한 메모리를 정리하는 식으로 어떻게 보면 굉장히 불안정한 메모리 공간이라 영구적으로 저장할 데이터를 넣기에는 바람직 하지 않다.

따라서 TLS를 만들어서 전역 메모리 처럼 사용하게 되는데. 나만 접근하는 전역메모리 라고 생각하면 쉬울 것 같다.

Rokiss님의 비유에 따르면 동물원에서 사자가 중앙에 있는 고기를 일부 가져가서 자기 자리에서 먹는 것을 생각하면 된다고 했는데 너무 적절한 비유라 가져와봤다.

thread_local int32 LThreadId = 0;

void ThreadMain(int32 threadId)
{
	LThreadId = threadId;

	while (true)
	{
		cout << "Thread " << LThreadId << " is working." << endl;
		this_thread::sleep_for(1s);
	}
}

int main()
{
	vector<thread> threads;
	for (int32 i = 0; i < 5; i++)
	{
		threads.emplace_back(ThreadMain, i + 1);
	}
	for (thread& t : threads)
	{
		t.join();
	}
}
  • 사용 방법은 굉장히 간단하다. 그냥 thread_local을 붙여서 선언해주면 각 스레드별로 독립적인 데이터를 이용하게 되는 것이다.

Thread Manager

profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글