컴퓨터 프로그래밍 공부/네트워크 서버

쓰레드 내부코드 보는 법

뽀또치즈맛 2025. 3. 5. 01:08

 

 
다음과 같은 join 코드의 내부 코드 보는 법
 
디버깅 돌려서 HelloThread() 함수에 중단점 찍기
 
중단점을 찍은 뒤 쓰레드를 주 스레드로 변경하면
내부코드 볼 수 있다.
 

 
뿐만 아니라
스레드 설정을 바꿔가며 다른 스레드들은 뭘 하고 있는지 왔다갔다하며
체크할 수 있다.
 

Thread 클래스 5가지 함수

 
사실 Thread 클래스와 관련된 내용들은 사실 복잡하지 않다.
Thread 클래스에서 5가지 핵심 함수를 알면,
그 외에는 크게 신경 쓸 일은 없다.

int main()
{
	// HelloThread();

	std::thread t(HelloThread);

	cout << "Hello Main" << endl;


	// 스레드 핵심 함수 5가지
	t.hardware_concurrency();
	t.get_id();
	t.detach();
	t.joinable();
	t.join();

}

 
 

std::thread hardware_concurrency(); -  CPU 코어 개수

멀티 코어 환경에서 실질적으로 구동할 수 있는 코어 개수가
어느 정도 CPU에 따라서 고정되어 있다.
그 정보를 추출하는 함수이다.
 
그런데, 이 함수는 100% 확률로 정확하지는 않다.
만약에 정보를 추출할 수 없다고 하면, 
경우에 따라서 0을 리턴하는 경우도 있다.
 
즉 반환 값으로는
하드웨어 스레드 컨텍스트의 수에 대한 추정치를 반환한다.
만약 해당 값이 계산될 수 없거나 명확하지 않다면, 이 메서드는 0을 반환한다.
 
사용예시 코드

	int32 count = t.hardware_concurrency(); // CPU 코어 개수

 

std::thread get_id(); - 각 쓰레드 마다 부여되는 ID

 
id라고 해서 순차적으로 1,2,3,4 씩 늘어나는 건 아니고
약간 들쑥날쑥한 값으로 늘어난다.
 
하지만 쓰레드 사이에는 겹치는 ID는 없다.
 
사용예시 코드

	auto id = t.get_id(); // 각 쓰레드 마다 부여되는 ID

 
 
 

std::thread join(); - 식별된 쓰레드 작업

 
join은 현재 쓰레드의 작업 흐름을 동기화로 수행하지 않고
관련 스레드가 완료될 때까지 차단한다.
 
호출이 성공하면 호출 개체에 get_id 대한 후속 호출
즉 기존 쓰레드와 같지 않은 thread::id 기본값을 반환한다.
호출이 성공하지 않으면 반환되는 get_id값은 변경되지 아니한다.
 
즉, join 구현 코드를 뜯어 보자면 
현재 쓰래드를 *this로 식별되는 스레드의 실행이 완효될 때까지 차단한다는 것이다.
*this로 식별되는 스레드의 완료는 join()의 성공적인 반환과 동기화된다.
 
*this 자체에 대해서는 동기화가 수행되지 않는다.
 
"join()은 *this 자체에 대해서 동기화를 수행하지 않는다"는 의미는,
join() 함수가 호출될 때 해당 스레드의 종료를 기다려주는 역할만 한다는 뜻이다.
즉, join()은 연결된 스레드의 종료와
그에 따른 메모리 동기화(예: 스레드가 수행한 작업의 결과를 호출 스레드에서 볼 수 있게 하는)를 보장하지만,
std::thread 객체(*this) 자체에 대해
내부적으로 별도의 동기화(예: 락이나 뮤텍스)를 적용하지 않는다는 말이다.
 
 

  • 스레드 종료 동기화:
  • join()은 대상 스레드가 종료될 때까지 현재 스레드를 대기시키며,
  • 스레드 종료 시 그 스레드에서 수행된 작업의 결과가 호출자에게 보이도록 메모리 동기화를 보장한다.
  • 객체 접근 동기화 미적용:
  • 하지만, join() 내부에서는
  • std::thread 객체(즉, join()을 호출한 객체)
  • 자체를 보호하기 위한 별도의 동기화 메커니즘이 적용되지 않는다.
  • 따라서 여러 스레드가 동시에 같은 std::thread 객체에 대해 join()을 호출하는 경우,
  • 이는 동기화되지 않은 상태에서 객체에 대한 동시 접근이 발생하여
  • 데이터 경쟁(race condition)이 생길 수 있으며,
  • 이는 정의되지 않은 동작(Undefined Behavior)을 초래한다.

 
std::thread 객체 자체에 대한 동시 접근 문제는 개발자가 별도로 관리해야 한다.
 
즉, 마지막 말인 
"std::thread 객체 자체에 대한 동시 접근 문제는 개발자가 별도로 관리해야 한다."는,
 
여러 스레드에서 동시에 동일한 스레드 객체에 대해 join()을 호출하는 것은
데이터 경쟁을 일으켜 정의되지 않은 동작(= 오류)을 초래한다.
라는 말과 같다.
 
++Plus++
왜 굳이 *this냐?
std::thread join도 결국 클래스이고 함수이다.
이 함수를 *this로 접근하게 되면
해당 함수의 개체 그 자체를 의미하고,
해당 개체의 모든 메서드, 멤버에 접근할 수 있기 때문에
*this를 쓴다.
++++
 
 
 

std::tread detach(); - 실제 스레드와 객체 분리

 
개발자가 만들어준 std::tread 객체 변수와
실질적으로 구동된 스레드랑 연결고리를 딱 끊어주는 것이다.
그러니까, 아예 백그라운드 스레드로 그냥 독립적으로 동작하는 개념이라고 보면 된다.
 
detach를 해주면 std thread 객체에서 
실제 스레드를 분리해준다.
이러한 것을 데몬 프로세스에 응용할 수 잇다.
 
하지만 Thread의 정보 추출이나 상태를 확인할 수 없기 때문에
사실 평소에는 사용할 일이 없다.
 
 

std::tread  joinable(); - 연결 가능 여부 확인

 
bool  값을 리턴하며
현재 연결된 스레드가 연결 가능한지의 여부를 확인해준다.
 
실제 정의된 함수 정의부분

bool joinable() const noexcept;

// 출저 - microsoft
// https://learn.microsoft.com/en-us/cpp/standard-library/thread-class?view=msvc-170#join

 
 
응용해서 생각해보자.
std::thread::detach의 경우에는
joinable은 false를 return하는 것이 정상 작동하는 것이다.
왜? 연결을 끊어주니까.
 
그럼 std::tread::join() 시, 
joinable이 false가 나오면 스레드가 유효하지 않다는 것이고
이는 오류 조건이 된다는 것이다.
 
 
그럼 이제,

	std::thread t;

이러한 경우는 Thread의 어떠한 기능을 할까?
정답은 아무런 기능을 하지 않는다.
실질적으로 Thread가 실행되지 않는 것이다.
 
무언가 함수를 호출하고 값을 읽어오고 호출하는 시도를 할 때
Thread 실행이 되는 것이다.
 

코드와 주석으로 개념 정리

#include "pch.h"
#include <iostream>
#include "CorePch.h"

#include <thread>

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

int main()
{
	// HelloThread();

	std::thread t; // (HelloThread);

	cout << "Hello Main" << endl;

	int32 count = t.hardware_concurrency(); // CPU 코어 개수
	auto id = t.get_id(); // 각 쓰레드 마다 부여되는 ID
	t.detach();	// 스레드와 개체의 분리
	t.joinable();	// 개체와 연결된 스레드가 join가능 한지?
	t.join();	// 개체와 바인딩된 스레드가 종료될 때 까지 기다려준다.


}

 
 
응용 코드

#include "pch.h"
#include <iostream>
#include "CorePch.h"

#include <thread>

void HelloThread(int32 num)
{
	cout << "Hello Thread" << num << endl;
}

int main()
{
	
	vector<std::thread> v;

	for (int32 i = 0; i < 10; i++) {
		v.push_back(std::thread(HelloThread, i));
	}

	for (int32 i = 0; i < 10; i++) {
		if (v[i].joinable()) {
			v[i].join();
		}
	}

	cout << "Hello Main" << endl;


}

 
 
실행 결과를 봐보자,

 
과연 이 실행 결과가 오류일까?
아니다.
딱히 순서를 정해주지도 않았다.
앞서 말한 바와 같이
join은 연결 여부를 확인하여 실행후 종료까지 기다려 줄 뿐이다.
즉 다시 한 번 말하지만,
join은 *this 자체에 대해서 동기화를 수행하지 않으므로
join 함수가 호출될 때 해당 스레드의 종료를 기다려주는 역할만 수행 한다.
 
즉, join()은 연결된 스레드의 종료와
그에 따른 메모리 동기화,
스레드가 수행한 작업의 결과를 호출 스레드에서 볼 수 있게 하는 것은 보장하지만,
 
std::thread 객체(*this) 자체에 대해
내부적으로 별도의 락이나 뮤텍스 같은
실행 흐름 단위의 동기화를 적용하지 않는다는 말이다.
 
 
 
참고 문서
https://en.cppreference.com/w/cpp/thread/thread/join

std::thread::join - cppreference.com

void join(); (since C++11) Blocks the current thread until the thread identified by *this finishes its execution. The completion of the thread identified by *this synchronizes with the corresponding successful return from join(). No synchronization is perf

en.cppreference.com

https://learn.microsoft.com/ko-kr/cpp/standard-library/thread-class?view=msvc-170

thread 클래스

자세한 정보: 스레드 클래스

learn.microsoft.com

 
https://en.cppreference.com/w/cpp/thread/thread/detach

std::thread::detach - cppreference.com

void detach(); (since C++11) Separates the thread of execution from the thread object, allowing execution to continue independently. Any allocated resources will be freed once the thread exits. After calling detach *this no longer owns any thread. [edit] P

en.cppreference.com