C++ 멀티스레딩 | "thread/mutex" 기초 가이드
이 글의 핵심
C++ 멀티스레딩에 대해 정리한 개발 블로그 글입니다. #include <iostream> #include <thread> using namespace std;
기본 스레드 생성
#include <iostream>
#include <thread>
using namespace std;
void printHello() {
cout << "Hello from thread!" << endl;
}
int main() {
thread t(printHello); // 스레드 생성
t.join(); // 스레드 종료 대기
cout << "Main thread" << endl;
return 0;
}
람다와 매개변수
#include <iostream>
#include <thread>
using namespace std;
int main() {
// 람다 사용
thread t1( {
cout << "Lambda thread" << endl;
});
// 매개변수 전달
thread t2( {
cout << x << ": " << s << endl;
}, 10, "Hello");
t1.join();
t2.join();
return 0;
}
mutex로 동기화
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
mtx.lock();
counter++;
mtx.unlock();
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "Counter: " << counter << endl; // 2000
return 0;
}
lock_guard (RAII)
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
void safeIncrement(int& counter) {
for (int i = 0; i < 1000; i++) {
lock_guard<mutex> lock(mtx); // 자동 unlock
counter++;
} // 자동으로 unlock됨
}
int main() {
int counter = 0;
thread t1(safeIncrement, ref(counter));
thread t2(safeIncrement, ref(counter));
t1.join();
t2.join();
cout << "Counter: " << counter << endl; // 2000
return 0;
}
실전 예시
예시 1: 병렬 계산
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void sumRange(int start, int end, long long& result) {
long long sum = 0;
for (int i = start; i < end; i++) {
sum += i;
}
result = sum;
}
int main() {
const int N = 100000000;
const int NUM_THREADS = 4;
vector<thread> threads;
vector<long long> results(NUM_THREADS);
int range = N / NUM_THREADS;
// 스레드 생성
for (int i = 0; i < NUM_THREADS; i++) {
int start = i * range;
int end = (i == NUM_THREADS - 1) ? N : (i + 1) * range;
threads.emplace_back(sumRange, start, end, ref(results[i]));
}
// 모든 스레드 대기
for (auto& t : threads) {
t.join();
}
// 결과 합산
long long total = 0;
for (long long r : results) {
total += r;
}
cout << "합계: " << total << endl;
return 0;
}
설명: 큰 계산을 여러 스레드로 나누어 병렬 처리합니다.
예시 2: 생산자-소비자 패턴
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
using namespace std;
queue<int> dataQueue;
mutex mtx;
condition_variable cv;
bool done = false;
void producer() {
for (int i = 1; i <= 10; i++) {
this_thread::sleep_for(chrono::milliseconds(100));
{
lock_guard<mutex> lock(mtx);
dataQueue.push(i);
cout << "생산: " << i << endl;
}
cv.notify_one(); // 소비자에게 알림
}
{
lock_guard<mutex> lock(mtx);
done = true;
}
cv.notify_all();
}
void consumer() {
while (true) {
unique_lock<mutex> lock(mtx);
cv.wait(lock, {
return !dataQueue.empty() || done;
});
while (!dataQueue.empty()) {
int value = dataQueue.front();
dataQueue.pop();
lock.unlock();
cout << "소비: " << value << endl;
this_thread::sleep_for(chrono::milliseconds(150));
lock.lock();
}
if (done && dataQueue.empty()) {
break;
}
}
}
int main() {
thread prod(producer);
thread cons(consumer);
prod.join();
cons.join();
return 0;
}
설명: 생산자와 소비자가 큐를 통해 데이터를 주고받는 패턴입니다.
예시 3: 스레드 풀
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std;
class ThreadPool {
private:
vector<thread> workers;
queue<function<void()>> tasks;
mutex mtx;
condition_variable cv;
bool stop;
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; i++) {
workers.emplace_back([this]() {
while (true) {
function<void()> task;
{
unique_lock<mutex> lock(mtx);
cv.wait(lock, [this]() {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) {
return;
}
task = move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
unique_lock<mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& worker : workers) {
worker.join();
}
}
void enqueue(function<void()> task) {
{
unique_lock<mutex> lock(mtx);
tasks.push(task);
}
cv.notify_one();
}
};
int main() {
ThreadPool pool(4);
for (int i = 1; i <= 10; i++) {
pool.enqueue([i]() {
cout << "작업 " << i << " 시작 (스레드 "
<< this_thread::get_id() << ")" << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "작업 " << i << " 완료" << endl;
});
}
this_thread::sleep_for(chrono::seconds(5));
return 0;
}
설명: 스레드 풀로 작업을 효율적으로 분배합니다.
자주 발생하는 문제
문제 1: 경쟁 조건 (Race Condition)
증상: 결과가 매번 다름
원인: 동기화 없이 공유 자원 접근
해결법:
// ❌ 경쟁 조건
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // 위험!
}
}
// ✅ mutex로 보호
mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 1000; i++) {
lock_guard<mutex> lock(mtx);
counter++;
}
}
문제 2: 데드락 (Deadlock)
증상: 프로그램이 멈춤
원인: 서로 다른 mutex를 기다림
해결법:
// ❌ 데드락 가능
mutex mtx1, mtx2;
void func1() {
lock_guard<mutex> lock1(mtx1);
lock_guard<mutex> lock2(mtx2);
}
void func2() {
lock_guard<mutex> lock2(mtx2); // 순서 다름!
lock_guard<mutex> lock1(mtx1);
}
// ✅ 항상 같은 순서로 lock
void func1() {
lock_guard<mutex> lock1(mtx1);
lock_guard<mutex> lock2(mtx2);
}
void func2() {
lock_guard<mutex> lock1(mtx1); // 같은 순서
lock_guard<mutex> lock2(mtx2);
}
// ✅ scoped_lock 사용 (C++17)
void func() {
scoped_lock lock(mtx1, mtx2); // 자동으로 데드락 방지
}
문제 3: detach 후 댕글링 참조
증상: 크래시 또는 이상한 값
원인: 스레드가 참조하는 변수가 소멸됨
해결법:
// ❌ 위험한 코드
void badExample() {
int data = 10;
thread t([&data]() {
this_thread::sleep_for(chrono::seconds(1));
cout << data << endl; // 이미 소멸!
});
t.detach();
} // data 소멸
// ✅ 값으로 캡처
void goodExample() {
int data = 10;
thread t([data]() {
this_thread::sleep_for(chrono::seconds(1));
cout << data << endl; // 안전
});
t.detach();
}
// ✅ join으로 대기
void betterExample() {
int data = 10;
thread t([&data]() {
cout << data << endl;
});
t.join(); // 대기
}
FAQ
Q1: join vs detach?
A:
- join: 스레드 종료 대기 (권장)
- detach: 백그라운드 실행 (주의 필요)
Q2: 몇 개의 스레드를 만들어야 하나요?
A: 일반적으로 CPU 코어 수만큼이 적절합니다.
unsigned int numThreads = thread::hardware_concurrency();
Q3: mutex는 느린가요?
A: 약간의 오버헤드가 있지만 필요한 경우 반드시 사용해야 합니다. atomic을 고려할 수도 있습니다.
Q4: 스레드 안전한 컨테이너는?
A: C++ 표준 컨테이너는 기본적으로 스레드 안전하지 않습니다. mutex로 보호하거나 concurrent 라이브러리를 사용하세요.
Q5: async vs thread?
A:
- thread: 저수준 제어
- async: 고수준, 간편 (future 반환)
auto future = async(launch::async, {
return 42;
});
cout << future.get() << endl;
Q6: 멀티스레딩은 언제 사용하나요?
A:
- CPU 집약적 작업 병렬화
- I/O 대기 시간 활용
- 반응성 향상 (UI)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ jthread | “자동 조인 스레드” 가이드
- C++ scoped_lock | “범위 락” 가이드
- C++ 스레드 풀 | “Thread Pool” 구현 가이드
관련 글
- C++ Atomic |
- C++ 멀티스레드 크래시 |
- C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
- C++ 기술 면접 질문 30선 |
- C++ jthread |