C++ 동시성 — thread·mutex·atomic
개요 — C++ 표준 동시성
Section titled “개요 — C++ 표준 동시성”C++11은 플랫폼 독립적인 스레드 지원을 표준에 포함했습니다. 이전에는 pthread(POSIX)나 WinAPI 스레드를 직접 사용했으나, 이제 <thread>, <mutex>, <atomic>, <future> 헤더로 이식성 있는 동시성 코드를 작성할 수 있습니다.
동시성의 핵심 문제:
- 데이터 경쟁(Data Race): 복수 스레드가 같은 데이터를 동시에 읽기+쓰기
- 교착 상태(Deadlock): 두 스레드가 서로의 락을 기다리며 멈춤
- 경쟁 조건(Race Condition): 실행 순서에 따라 결과가 달라짐
1. std::thread — 스레드 생성
Section titled “1. std::thread — 스레드 생성”#include <thread>#include <iostream>
// 함수로 스레드 생성void WorkerFunction(int Id, const std::string& Name){ std::cout << "스레드 " << Id << ": " << Name << "\n";}
int main(){ // 스레드 생성 — 즉시 실행 시작 std::thread T1(WorkerFunction, 1, "Alice"); std::thread T2(WorkerFunction, 2, "Bob");
// join: 해당 스레드가 완료될 때까지 현재 스레드 대기 T1.join(); T2.join();
return 0;}// 람다로 스레드 생성std::vector<int> Data = {1, 2, 3, 4, 5};int Sum = 0;
std::thread Worker([&Data, &Sum](){ for (int N : Data) { Sum += N; }});
Worker.join();// 주의: Sum에 대한 동시 접근 시 데이터 경쟁 발생 — 이 예시는 스레드가 1개라 안전join vs detach
Section titled “join vs detach”std::thread T([]() { /* ... */ });
// join: 완료 대기 (T가 소멸 전 반드시 join 또는 detach)T.join();
// detach: 백그라운드에서 분리 실행 (완료 대기 안 함)// T.detach(); // T의 수명과 무관하게 실행 — 주의 필요
// jthread (C++20): 소멸 시 자동 joinstd::jthread JT([]() { /* ... */ }); // 소멸자에서 자동 join스레드 유틸리티
Section titled “스레드 유틸리티”// 현재 실행 중인 스레드 정보std::thread::id MainId = std::this_thread::get_id();
// 하드웨어 동시 실행 가능 스레드 수unsigned int HWThreads = std::thread::hardware_concurrency();
// 현재 스레드 양보 (다른 스레드에 실행 기회)std::this_thread::yield();
// 현재 스레드 잠재우기std::this_thread::sleep_for(std::chrono::milliseconds(100));std::this_thread::sleep_until(std::chrono::steady_clock::now() + std::chrono::seconds(1));2. std::mutex — 상호 배제
Section titled “2. std::mutex — 상호 배제”mutex(Mutual Exclusion)는 한 번에 하나의 스레드만 보호 구역에 진입하도록 합니다.
#include <mutex>
std::mutex DataMutex;int SharedCounter = 0;
void Increment(){ for (int i = 0; i < 1000; ++i) { DataMutex.lock(); // 진입 시도 (이미 잠겼으면 대기) ++SharedCounter; DataMutex.unlock(); // 반드시 해제 (예외 시 누락 위험) }}std::lock_guard — RAII 락
Section titled “std::lock_guard — RAII 락”void SafeIncrement(){ for (int i = 0; i < 1000; ++i) { std::lock_guard<std::mutex> Lock(DataMutex); // Lock 생성 시 lock(), 소멸(스코프 종료) 시 자동 unlock() ++SharedCounter; // 예외가 발생해도 소멸자에서 unlock() 보장 }}
// C++17: std::scoped_lock — 복수 mutex 동시 락 (교착 방지)std::mutex MutexA, MutexB;
void SafeSwap(){ std::scoped_lock Lock(MutexA, MutexB); // 교착 없이 둘 다 락 // ...}std::unique_lock — 유연한 락
Section titled “std::unique_lock — 유연한 락”std::mutex M;
void FlexibleLock(){ std::unique_lock<std::mutex> Lock(M); // 즉시 락
// 조건부 해제/재락 Lock.unlock(); // 잠시 해제 // ... 락 없이 작업 ... Lock.lock(); // 재락
// 소멸 시 자동 unlock}
// 지연 락std::unique_lock<std::mutex> Lock(M, std::defer_lock);// ... 다른 작업 ...Lock.lock(); // 나중에 락std::shared_mutex — 읽기/쓰기 분리 (C++17)
Section titled “std::shared_mutex — 읽기/쓰기 분리 (C++17)”#include <shared_mutex>
std::shared_mutex RWMutex;std::unordered_map<int, std::string> Cache;
// 읽기 — 복수 스레드 동시 허용std::string Read(int Key){ std::shared_lock<std::shared_mutex> Lock(RWMutex); // 공유 락 auto It = Cache.find(Key); return (It != Cache.end()) ? It->second : "";}
// 쓰기 — 단독 접근void Write(int Key, const std::string& Value){ std::unique_lock<std::shared_mutex> Lock(RWMutex); // 배타 락 Cache[Key] = Value;}3. std::atomic — 원자적 연산
Section titled “3. std::atomic — 원자적 연산”뮤텍스 없이 단순 타입(int, bool, 포인터 등)을 스레드 안전하게 조작합니다.
#include <atomic>
std::atomic<int> AtomicCounter{0};std::atomic<bool> bRunning{true};
// 원자적 증가 (데이터 경쟁 없음)void Worker(){ while (bRunning.load()) { AtomicCounter.fetch_add(1, std::memory_order_relaxed); // 또는 간단하게 ++AtomicCounter; }}
// 값 읽기/쓰기int Value = AtomicCounter.load();AtomicCounter.store(100);int Old = AtomicCounter.exchange(200); // 이전 값 반환 후 교체CAS (Compare-And-Swap)
Section titled “CAS (Compare-And-Swap)”std::atomic<int> A{10};
// A가 10이면 20으로 교체, 아니면 실패 (Expected가 실제값으로 업데이트됨)int Expected = 10;bool bSuccess = A.compare_exchange_strong(Expected, 20);
// weak 버전 — 허위 실패 가능하지만 루프에서 더 효율적while (!A.compare_exchange_weak(Expected, 20)){ // Expected가 현재값으로 업데이트됨 — 재시도}atomic vs mutex 선택 기준
Section titled “atomic vs mutex 선택 기준”| 상황 | 선택 |
|---|---|
| 단순 카운터·플래그 | std::atomic |
| 복잡한 데이터 구조 보호 | std::mutex |
| 읽기 多, 쓰기 少 | std::shared_mutex |
| 여러 변수를 한 번에 보호 | std::mutex |
4. std::future / std::promise — 비동기 결과 전달
Section titled “4. std::future / std::promise — 비동기 결과 전달”#include <future>
// promise: 값을 설정하는 쪽// future: 값을 받는 쪽
std::promise<int> Promise;std::future<int> Future = Promise.get_future();
// 별도 스레드에서 값 설정std::thread Producer([&Promise](){ std::this_thread::sleep_for(std::chrono::seconds(1)); Promise.set_value(42); // 결과 전달});
// 메인 스레드에서 결과 대기int Result = Future.get(); // 블로킹 — 값이 올 때까지 대기std::cout << "결과: " << Result; // 42
Producer.join();// 예외 전달std::promise<int> ErrPromise;std::future<int> ErrFuture = ErrPromise.get_future();
std::thread ErrorProducer([&ErrPromise](){ try { throw std::runtime_error("계산 실패"); } catch (...) { ErrPromise.set_exception(std::current_exception()); // 예외 전달 }});
try{ int Val = ErrFuture.get(); // 예외가 여기서 다시 throw됨}catch (const std::exception& E){ std::cout << "오류: " << E.what();}
ErrorProducer.join();5. std::async — 비동기 작업 실행
Section titled “5. std::async — 비동기 작업 실행”std::async는 스레드 생성과 future 반환을 한 번에 처리합니다.
#include <future>
// async로 백그라운드 실행std::future<int> Future = std::async(std::launch::async, [](){ // 백그라운드 스레드에서 실행 return HeavyComputation();});
// 다른 작업 수행...DoOtherWork();
// 결과 가져오기 (완료 대기)int Result = Future.get();launch 정책
Section titled “launch 정책”// std::launch::async — 새 스레드에서 즉시 실행auto F1 = std::async(std::launch::async, Task);
// std::launch::deferred — get() 호출 시 현재 스레드에서 실행 (지연)auto F2 = std::async(std::launch::deferred, Task);
// 기본값 (async | deferred) — 구현 의존적auto F3 = std::async(Task);std::async 병렬 처리 예시
Section titled “std::async 병렬 처리 예시”// 큰 배열을 두 스레드로 분할 합산std::vector<int> Data(1000000);std::iota(Data.begin(), Data.end(), 1);
auto Half = Data.size() / 2;
// 두 구간을 병렬로 합산auto Future1 = std::async(std::launch::async, [&](){ return std::accumulate(Data.begin(), Data.begin() + Half, 0LL);});
auto Future2 = std::async(std::launch::async, [&](){ return std::accumulate(Data.begin() + Half, Data.end(), 0LL);});
long long Total = Future1.get() + Future2.get();6. std::condition_variable — 스레드 간 신호
Section titled “6. std::condition_variable — 스레드 간 신호”#include <condition_variable>
std::mutex Mutex;std::condition_variable CV;bool bDataReady = false;int SharedData = 0;
// 생산자void Producer(){ { std::lock_guard<std::mutex> Lock(Mutex); SharedData = 42; bDataReady = true; } CV.notify_one(); // 대기 중인 스레드 하나 깨우기}
// 소비자void Consumer(){ std::unique_lock<std::mutex> Lock(Mutex);
// bDataReady가 true가 될 때까지 대기 (허위 깨어남 방지) CV.wait(Lock, []() { return bDataReady; });
// 이 시점에 락을 재취득하고 실행 std::cout << "데이터: " << SharedData;}7. 스레드 안전 설계 패턴
Section titled “7. 스레드 안전 설계 패턴”7.1 스레드 안전 큐
Section titled “7.1 스레드 안전 큐”template<typename T>class ThreadSafeQueue{public: void Push(T Value) { std::lock_guard<std::mutex> Lock(Mutex); Queue.push(std::move(Value)); CV.notify_one(); }
bool TryPop(T& OutValue) { std::lock_guard<std::mutex> Lock(Mutex); if (Queue.empty()) return false; OutValue = std::move(Queue.front()); Queue.pop(); return true; }
T WaitAndPop() { std::unique_lock<std::mutex> Lock(Mutex); CV.wait(Lock, [this]() { return !Queue.empty(); }); T Value = std::move(Queue.front()); Queue.pop(); return Value; }
private: std::queue<T> Queue; std::mutex Mutex; std::condition_variable CV;};| 도구 | 역할 |
|---|---|
std::thread | OS 스레드 생성·관리 |
std::jthread (C++20) | 자동 join 스레드 |
std::mutex | 상호 배제 (단독 접근) |
std::shared_mutex | 읽기 공유·쓰기 단독 |
std::lock_guard | RAII 뮤텍스 락 |
std::unique_lock | 유연한 뮤텍스 락 |
std::atomic<T> | 락 없는 원자적 연산 |
std::future/promise | 스레드 간 값·예외 전달 |
std::async | 비동기 작업 + future |
std::condition_variable | 스레드 간 이벤트 신호 |
핵심 규칙:
- 뮤텍스는 항상
lock_guard/scoped_lock으로 RAII 관리 — 수동unlock()금지 std::thread는 소멸 전 반드시join()또는detach()호출- 단순 카운터·플래그는
std::atomic, 복합 데이터는std::mutex - 교착 상태 방지: 항상 같은 순서로 락 획득 또는
std::scoped_lock사용