← SEARCHABLE ARCHIVE
C / C++ · 2026-08-14

C++ 기초부터 마스터까지 — 20화

스레드, atomic과 메모리 모델로 동시성 다루기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

여러 스레드가 같은 메모리에 접근하면 단순히 실행 순서가 달라지는 것보다 더 큰 문제가 생긴다. C++ 메모리 모델에서 동기화되지 않은 data race는 정의되지 않은 동작이다.

공유 상태는 명시적으로 보호한다

#include <mutex>

class Inventory
{
public:
    void Add(Item item)
    {
        std::lock_guard lock(Mutex);
        Items.push_back(std::move(item));
    }

private:
    std::mutex Mutex;
    std::vector<Item> Items;
};

std::lock_guard는 범위를 벗어나면 잠금을 해제하므로 예외 경로도 안전하다. 잠금은 벡터의 한 연산만이 아니라 함께 유지해야 하는 불변식 전체를 보호해야 한다.

여러 mutex를 서로 다른 순서로 획득하면 deadlock이 생길 수 있다. std::scoped_lock으로 함께 잠그거나 프로젝트 전체의 잠금 순서를 정해야 한다.

atomic은 작은 공유 값에 적합하다

#include <atomic>

std::atomic<unsigned int> completedJobs{0};

void MarkCompleted()
{
    completedJobs.fetch_add(1, std::memory_order_relaxed);
}

단순 통계 카운터처럼 다른 데이터의 공개 순서를 제어하지 않는 값은 relaxed 연산으로 충분할 수 있다. 하지만 atomic 카운터 하나가 여러 필드로 이루어진 객체의 일관성을 자동으로 보호하지는 않는다.

기본 순서인 sequential consistency는 이해하기 쉬운 단일 전역 순서를 제공한다. acquire, release, relaxed 같은 약한 순서는 정확한 happens-before 관계를 증명한 뒤에만 사용해야 한다. 성능 추측으로 낮추면 재현하기 어려운 버그가 생긴다.

jthread로 종료 수명 묶기

#include <chrono>
#include <stop_token>
#include <thread>

void Worker(std::stop_token stop)
{
    while (!stop.stop_requested())
    {
        ProcessOneJob();
    }
}

void Run()
{
    std::jthread worker(Worker);
    // 범위를 벗어나면 중지를 요청하고 join한다.
}

C++20의 std::jthread는 소멸할 때 중지를 요청하고 스레드 종료를 기다린다. stop_token은 강제 종료가 아니라 협력적 취소다. blocking I/O가 중지 요청에 반응하지 않으면 별도의 timeout이나 깨우기 방법이 필요하다.

경쟁을 제거하는 설계가 먼저다

작업마다 독립된 입력을 주고 결과를 메시지나 queue로 모으면 공유 변경 상태와 잠금 범위를 줄일 수 있다. 같은 캐시 라인의 서로 다른 atomic을 여러 코어가 계속 수정하면 false sharing으로 성능이 낮아질 수도 있다.

동시성 코드는 테스트 한 번 통과했다고 안전하지 않다. 소유권, 종료 조건, 잠금 순서, 메모리 공개 규칙을 문서화하고 ThreadSanitizer와 반복 스트레스 테스트로 가정을 검증해야 한다.