스레드 관리

박성빈·2024년 12월 29일
0

Cpp

목록 보기
2/5

C++에서 표준 thread 라이브러리의 사용법을 공부한다.

스레드 시작과 대기

스레드는 해당 스레드에서 실행할 작업을 지정하는 std::thread 객체를 초기화 하며 시작한다.
스레드 엔트리 함수를 단순한 함수로 전달할 수도 있고, 함수 호출 연산자가 있는 인스턴스(함수 객체)를 전달할 수도 있고, 람다를 전달할 수도 있다.
엔트리 함수를 전달하지 않고 std::thread를 생성하면 그냥 인스턴스만 생성할 뿐 스레드가 생성되지 않는다.

#include <thread>

void threadFunc()
{
	std::cout << "Hello" << std::endl;
}

int main()
{
	//스레드 시작
	std::thread myThread(threadFunc);
    //스레드 대기
    myThread.join();
    
    return 1;
}

간단하게 위 코드처럼 스레드를 생성하고 생성된 스레드가 종료될 때까지 대기 시킬 수 있다.
join()을 하지 않으면 main의 return을 만나 스레드가 진행 중인데도 프로그램이 종료될 수 있다.
그리고 join()을 호출하게 되면 해당 std::thread에서 생성한 스레드와 연관이 끊기게 된다. (std::thread 인스턴스와 생성된 스레드는 다른 것이다.)
이런 사실은 당연하다. join에서 리턴된다는 것이 해당 스레드가 종료되었다는 것이기 때문이다.

std::thread 인스턴스에 대해 두 번 join()을 수행하면 에러가 발생한다.
그래서 join()을 호출할 수 있는 인스턴스인지 확인하려면 joinable()함수를 호출해서 체크를 하면 된다.

std::thread는 복사는 불가능 하고 이동 연산은 가능하다.
이는 하나의 스레드 관리를 여러 스레드가 복사해서 가지고 있다면 말이 안 되기 때문에 unique_ptr처럼 단일 소유만 가능하게 구현한 것이다.

detach

스레드를 백그라운드에서 돌게 하기 위해서 std::thread 객체와 의도적으로 연관을 끊는 detach()함수도 있다.
detach는 std::thread오브젝트에서 스레드를 분리하는 개념이다.
이렇게 분리를 하게 되면 std::thread 오브젝트가 정리되어도 스레드의 실행은 유지된다.

detach를 사용하는 상황:

  1. 백그라운드 작업으로 실행되는 스레드가 완전히 독립적이고, 결과를 확인하거나 제어할 필요가 없는 경우.
  2. 메인 스레드가 스레드 종료를 기다리지 않고 다른 작업을 계속해야 할 때.

하지만 거의 대부분의 경우에는 detach를 하지 않는다.
스레드를 스레드 오브젝트에서 분리하게 되면 그 스레드를 더 이상 제어하거나 관련 정보를 얻을 수 없기 때문이다. 그래서 일반적으로 detach를 하지 않고 전역 스코프에 std::thread 객체를 둬서 로컬 범위 바깥으로 가도 std::thread가 소멸되지 않도록 한다.

스레드 ID

각각의 스레드는 ID를 가진다.

std::this_thread::get_id();

이렇게 현재 스레드의 id를 얻을 수도 있다.

스레드에 매개변수 전달하기

매우 간단하다.
그냥 std::thread 생성자에 추가적으로 인자를 전달하면 된다.

void func(int a)
{
	std::cout << a << std::endl;
}

int main()
{
	int num = 29;
	std::thread t1(func, num);
    t1.join();
}

매개변수가 한 개 이상이어도 그냥 뒤에 쭉 이어서 넣어주면 된다.

만약 엔트리 함수의 매개변수가 참조라면 std::ref를 써서 인자를 전달해야 한다.

void func(int a, std::string& str)
{
	std::cout << a << ' ' << str <<std::endl;
}

int main()
{
	int num = 29;
    std::string str = "hello";
	std::thread t1(func, num, std::ref(str));
    t1.join();
}

참조를 인자로 전달할 때 신경 써야하는 점이 있다.
그것은 해당 변수의 생명 주기이다.

void func(int i, std::string const& s);

void foo(int some_param)
{
	char buffer[1024];
    sprintf(buffer, "%d", some_param);
    std::thread(func, 3, buffer);
}

이렇게 지역 변수인 buffer를 전달하면 배열은 첫 번째 원소의 포인터이기 때문에 전달된 인자들은 복사가 된다.
새로 생성된 스레드의 실행이 func 함수를 실행시키기 전에 buffer가 가리키는 내용이 소멸했을 수도 있다.
그러면 넘겨진 bufferchar* 타입이기 때문에 새로운 스레드가 func를 호출할 때 매개변수 s의 타입인 std::string으로 타입 변환이 이뤄지는데 그 전에 foo함수가 끝나버리면 buffer가 가리키는 값이 쓰레기 값이 되어버려서 정의되지 않는 동작이 발생한다.

이런 문제를 피하기 위해서는 buffer를 미리 std::string객체로 초기화 시켜서 인자로 전달하면 된다.
std::thread(func, 3, std::stirng(buffer));

profile
게임 서버 프로그래밍을 공부하고 있습니다.

0개의 댓글