표준 라이브러리를 활용한 멀티스레딩
C++11부터 제공되는 표준 스레드 라이브러리는 플랫폼에 독립적인 동시성 프로그래밍을 가능하게 한다. 필요한 헤더는<thread>, <mutex>, <atomic>, <condition_variable> 등이며, 이들을 조합해 스레드 생성, 상호배제, 동기화, 원자적 연산 등을 구현할 수 있다.
스레드 생성과 실행 생명주기 관리
std::thread 객체를 생성하면 즉시 새로운 실행 흐름이 시작된다. 주요 두 가지 정책으로 자식 스레드의 종료를 처리할 수 있다:
- join(): 호출 스레드가 해당 스레드의 종료를 명시적으로 대기한다.
- detach(): 스레드를 분리 상태로 전환하여 독립적으로 실행되게 하며, 종료 시 자동 정리된다.
#include <iostream>
#include <thread>
#include <chrono>
void worker() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "작업 스레드 실행 완료\n";
}
int main() {
std::thread t(worker);
t.join(); // 메인 스레드가 t 종료까지 대기
std::cout << "메인 스레드 종료\n";
return 0;
}
공유 자원 보호: 뮤텍스와 잠금 관리
여러 스레드가 동시에 접근하는 공유 변수는 데이터 경합(race condition)을 일으킬 수 있다. 이를 방지하기 위해std::mutex와 함께 RAII 기반의 잠금 클래스를 사용한다.
예를 들어, 세 개의 티켓 판매 창구가 동시에 티켓 수량을 감소시키는 상황에서, 임계 영역(critical section)을 보호해야 한다.
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int remainingTickets = 50;
std::mutex ticketMutex;
void sellTicket(int windowId) {
while (true) {
{
std::lock_guard<std::mutex> lock(ticketMutex);
if (remainingTickets > 0) {
std::cout << "창구 " << windowId
<< ": " << remainingTickets--
<< "번 티켓 판매\n";
} else {
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
int main() {
std::vector<std::thread> windows;
for (int i = 1; i <= 3; ++i) {
windows.emplace_back(sellTicket, i);
}
for (auto& w : windows) {
w.join();
}
std::cout << "모든 티켓 판매 완료\n";
return 0;
}
스레드 간 동기화: 조건 변수
뮤텍스는 배제만을 제공하지만, 특정 조건이 충족될 때까지 스레드를 대기시키려면std::condition_variable이 필요하다. 생산자-소비자 패턴에서 유용하게 사용된다.
다음은 큐가 비었을 때 소비자가 대기하고, 생산자가 항목을 넣으면 소비자를 깨우는 예제이다.
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
class ThreadSafeQueue {
private:
std::queue<int> dataQueue;
mutable std::mutex queueMutex;
std::condition_variable itemAvailable;
public:
void produce(int value) {
std::unique_lock<std::mutex> lock(queueMutex);
itemAvailable.wait(lock, [this]() { return dataQueue.empty(); });
dataQueue.push(value);
std::cout << "[생산] " << value << "번 아이템 생성\n";
itemAvailable.notify_all();
}
int consume() {
std::unique_lock<std::mutex> lock(queueMutex);
itemAvailable.wait(lock, [this]() { return !dataQueue.empty(); });
int value = dataQueue.front();
dataQueue.pop();
std::cout << "[소비] " << value << "번 아이템 처리\n";
itemAvailable.notify_all();
return value;
}
};
void producerWorker(ThreadSafeQueue* q) {
for (int i = 1; i <= 5; ++i) {
q->produce(i);
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
void consumerWorker(ThreadSafeQueue* q) {
for (int i = 1; i <= 5; ++i) {
q->consume();
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
}
int main() {
ThreadSafeQueue queue;
std::thread p(producerWorker, &queue);
std::thread c(consumerWorker, &queue);
p.join();
c.join();
return 0;
}
std::unique_lock은 std::lock_guard보다 유연하며, 조건 변수와 함께 사용할 수 있고, 잠금 해제/획득을 수동으로 제어할 수 있다.
무임장 연산: std::atomic을 통한 성능 최적화
뮤텍스는 커널 리소스를 사용하므로 오버헤드가 크다. 단순한 증감 연산 같은 경우std::atomic을 사용하면 CAS(compare-and-swap) 기반의 무임장(임계영역 없음) 동기화가 가능하다.
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic<bool> ready{false};
std::atomic<int> counter{0};
void workerTask() {
while (!ready) {
std::this_thread::yield(); // CPU 양보
}
for (int i = 0; i < 1000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 10; ++i) {
workers.emplace_back(workerTask);
}
std::this_thread::sleep_for(std::chrono::milliseconds(500));
ready = true;
for (auto& w : workers) {
w.join();
}
std::cout << "최종 카운터 값: " << counter.load() << "\n";
return 0;
}
volatile은 C++에서 메모리 가시성 보장을 위해 사용되지 않으며, 대신 std::atomic과 메모리 순서 지정자(memory order)를 사용해야 한다.