Skip to content

C++ 동시성 — thread·mutex·atomic

C++11은 플랫폼 독립적인 스레드 지원을 표준에 포함했습니다. 이전에는 pthread(POSIX)나 WinAPI 스레드를 직접 사용했으나, 이제 <thread>, <mutex>, <atomic>, <future> 헤더로 이식성 있는 동시성 코드를 작성할 수 있습니다.

동시성의 핵심 문제:

  • 데이터 경쟁(Data Race): 복수 스레드가 같은 데이터를 동시에 읽기+쓰기
  • 교착 상태(Deadlock): 두 스레드가 서로의 락을 기다리며 멈춤
  • 경쟁 조건(Race Condition): 실행 순서에 따라 결과가 달라짐

#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개라 안전
std::thread T([]() { /* ... */ });
// join: 완료 대기 (T가 소멸 전 반드시 join 또는 detach)
T.join();
// detach: 백그라운드에서 분리 실행 (완료 대기 안 함)
// T.detach(); // T의 수명과 무관하게 실행 — 주의 필요
// jthread (C++20): 소멸 시 자동 join
std::jthread JT([]() { /* ... */ }); // 소멸자에서 자동 join
// 현재 실행 중인 스레드 정보
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));

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(); // 반드시 해제 (예외 시 누락 위험)
}
}
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::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;
}

뮤텍스 없이 단순 타입(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); // 이전 값 반환 후 교체
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가 현재값으로 업데이트됨 — 재시도
}
상황선택
단순 카운터·플래그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();

std::async는 스레드 생성과 future 반환을 한 번에 처리합니다.

#include <future>
// async로 백그라운드 실행
std::future<int> Future = std::async(std::launch::async, []()
{
// 백그라운드 스레드에서 실행
return HeavyComputation();
});
// 다른 작업 수행...
DoOtherWork();
// 결과 가져오기 (완료 대기)
int Result = Future.get();
// 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::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;
}

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::threadOS 스레드 생성·관리
std::jthread (C++20)자동 join 스레드
std::mutex상호 배제 (단독 접근)
std::shared_mutex읽기 공유·쓰기 단독
std::lock_guardRAII 뮤텍스 락
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 사용