C++ thread_local | "스레드 로컬 저장소" 가이드
이 글의 핵심
C++ thread_local에 대한 실전 가이드입니다.
들어가며
C++11의 thread_local은 각 스레드마다 독립적인 저장소를 제공하여 스레드 안전한 코드를 작성할 수 있게 합니다. 멀티스레드 환경에서 동기화 없이 스레드별 데이터를 관리할 수 있습니다.
1. thread_local 기본
개념
#include <thread>
#include <iostream>
thread_local int counter = 0;
void func() {
counter++;
std::cout << "스레드 " << std::this_thread::get_id()
<< ": " << counter << std::endl;
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
}
기본 사용
#include <thread>
#include <iostream>
thread_local int x = 0;
void worker() {
x++;
std::cout << "스레드 " << std::this_thread::get_id()
<< ": " << x << std::endl;
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
}
2. 실전 예제
예제 1: 스레드별 카운터
#include <thread>
#include <vector>
#include <iostream>
thread_local size_t requestCount = 0;
void handleRequest() {
requestCount++;
std::cout << "스레드 " << std::this_thread::get_id()
<< " 요청: " << requestCount << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; i++) {
threads.emplace_back( {
for (int j = 0; j < 3; j++) {
handleRequest();
}
});
}
for (auto& t : threads) {
t.join();
}
}
예제 2: 스레드별 버퍼
#include <thread>
#include <vector>
#include <iostream>
thread_local std::vector<int> buffer;
void flush(const std::vector<int>& buf) {
std::cout << "Flush: " << buf.size() << " items" << std::endl;
}
void process(int value) {
buffer.push_back(value);
if (buffer.size() >= 100) {
flush(buffer);
buffer.clear();
}
}
int main() {
std::thread t1( {
for (int i = 0; i < 150; i++) {
process(i);
}
});
t1.join();
}
예제 3: 난수 생성기
#include <random>
#include <thread>
#include <iostream>
thread_local std::mt19937 rng(std::random_device{}());
int getRandomNumber() {
std::uniform_int_distribution<int> dist(1, 100);
return dist(rng);
}
int main() {
std::thread t1( {
for (int i = 0; i < 5; i++) {
std::cout << "스레드 1: " << getRandomNumber() << std::endl;
}
});
std::thread t2( {
for (int i = 0; i < 5; i++) {
std::cout << "스레드 2: " << getRandomNumber() << std::endl;
}
});
t1.join();
t2.join();
}
3. 초기화
스레드 시작 시
#include <thread>
#include <iostream>
thread_local int x = 10;
void worker() {
std::cout << "x = " << x << std::endl;
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
}
첫 사용 시
#include <thread>
#include <iostream>
int compute() {
std::cout << "compute() 호출" << std::endl;
return 42;
}
void func() {
thread_local int y = compute();
std::cout << "y = " << y << std::endl;
}
int main() {
std::thread t1( {
func();
func();
});
t1.join();
}
4. 자주 발생하는 문제
문제 1: 소멸 순서
#include <thread>
#include <iostream>
struct Resource {
~Resource() {
std::cout << "Resource 소멸" << std::endl;
}
};
thread_local Resource r;
void func() {
std::cout << "func() 실행" << std::endl;
}
int main() {
std::thread t1(func);
t1.join();
}
문제 2: 클래스 멤버
#include <iostream>
class MyClass {
public:
static thread_local int x;
};
thread_local int MyClass::x = 0;
int main() {
MyClass::x = 42;
std::cout << MyClass::x << std::endl; // 42
}
문제 3: 초기화 비용
#include <memory>
#include <iostream>
struct ExpensiveObject {
ExpensiveObject() {
std::cout << "ExpensiveObject 생성" << std::endl;
}
};
thread_local std::unique_ptr<ExpensiveObject> obj;
void func() {
if (!obj) {
obj = std::make_unique<ExpensiveObject>();
}
}
int main() {
func();
func();
}
문제 4: 메모리 사용
#include <vector>
#include <thread>
#include <iostream>
thread_local std::vector<int> largeBuffer(1000000);
void worker() {
std::cout << "Buffer size: " << largeBuffer.size() << std::endl;
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
}
5. 사용 패턴
패턴 1: 스레드별 캐시
#include <unordered_map>
#include <string>
thread_local std::unordered_map<std::string, int> cache;
int getValue(const std::string& key) {
if (cache.find(key) != cache.end()) {
return cache[key];
}
int value = computeValue(key);
cache[key] = value;
return value;
}
패턴 2: 스레드별 통계
#include <iostream>
struct Statistics {
size_t count = 0;
size_t errors = 0;
void print() {
std::cout << "Count: " << count << ", Errors: " << errors << std::endl;
}
};
thread_local Statistics stats;
void processRequest() {
stats.count++;
}
정리
핵심 요약
- thread_local: 스레드별 독립 변수
- 초기화: 스레드 시작 또는 첫 사용 시
- 용도: 캐시, 통계, 난수 생성기
- 성능: 접근 빠름, 초기화 비용 있음
- 메모리: 스레드 수 × 변수 크기
thread_local vs 전역 변수
| 특성 | thread_local | 전역 변수 |
|---|---|---|
| 스레드 안전 | O | X |
| 동기화 필요 | X | O |
| 메모리 | 스레드당 | 1개 |
| 성능 | 빠름 | 동기화 필요 시 느림 |
실전 팁
- 스레드별 캐시에 활용
- 난수 생성기는 thread_local 사용
- 초기화 비용 고려
- 메모리 사용량 주의
다음 단계
- C++ jthread
- C++ random_device
- C++ Mutex
관련 글
- C++ async & launch |