C++20 std::barrier와 std::latch: 스레드 동기화 지점 만들기

이 글의 핵심

condition_variable과 카운터로 직접 만들던 동기화 지점을 latch와 barrier로 바꾸면 코드가 짧아지지만, latch는 재사용할 수 없고 barrier는 참여 스레드 수를 바꾸기 어렵다는 제약이 있습니다. 카운트 불일치로 영원히 대기하는 문제, 예외가 났을 때의 안전성, arrive_and_drop 사용법을 짚어 두 도구를 고르는 기준을 제공합니다.

들어가며

C++20은 std::latch와 std::barrier라는 새로운 동기화 도구를 도입했습니다. latch는 일회성 카운트다운으로 초기화 대기에 적합하며, barrier는 반복 동기화로 단계별 처리에 유용합니다.

C++20 이전에는 “N개의 스레드가 모두 준비될 때까지 기다린다”는 단순한 요구에도 mutex, condition_variable, 카운터를 조합해야 했습니다. 이 조합은 조건을 루프로 다시 확인하지 않아 생기는 가짜 깨어남(spurious wakeup), 알림을 대기보다 먼저 보내서 생기는 놓친 알림 같은 실수가 끼어들기 쉬운 코드입니다. latch와 barrier는 이 패턴을 표준 타입으로 굳힌 것으로, 무엇을 기다리는지가 타입 이름에 드러나 코드 리뷰도 쉬워집니다. 컴파일러는 GCC 11, Clang 14(libc++), MSVC 19.28 이후 버전에서 -std=c++20으로 사용할 수 있습니다.


기본 개념

latch vs barrier

특징latchbarrier
재사용❌ 일회성✅ 반복 가능
카운트감소만자동 리셋
완료 콜백❌✅
사용 시나리오초기화 대기단계별 동기화

기본 사용

#include <latch>
#include <barrier>
// latch: 한 번만
std::latch done(3);
done.count_down();
done.wait();
// barrier: 재사용 가능
std::barrier sync(3);
sync.arrive_and_wait();
sync.arrive_and_wait();  // OK

실전 구현

std::latch - 일회성 카운트다운

시그니처:

class latch {
public:
    explicit latch(ptrdiff_t expected);
    void count_down(ptrdiff_t n = 1);
    bool try_wait() const noexcept;
    void wait() const;
    void arrive_and_wait(ptrdiff_t n = 1);
};

기본 사용

#include <latch>
#include <thread>
#include <iostream>
#include <chrono>
int main() {
    std::latch done(3);
    
    auto worker = [&done](int id) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100 * id));
        std::cout << "워커 " << id << " 완료" << std::endl;
        done.count_down();
    };
    
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);
    
    std::cout << "모든 워커 대기 중..." << std::endl;
    done.wait();  // 0이 될 때까지 대기
    std::cout << "모두 완료" << std::endl;
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

이 예제에서 count_down()을 호출하는 쪽(워커)과 wait()를 호출하는 쪽(메인)은 서로 다른 스레드입니다. latch의 핵심 특징이 바로 이 비대칭성입니다. 카운트를 줄이는 스레드는 기다리지 않고 바로 다음 일을 할 수 있고, 기다리는 스레드는 카운트를 줄이지 않아도 됩니다. 또 한 스레드가 count_down(2)처럼 여러 번 몫을 한꺼번에 줄일 수도 있어서, “스레드 수”가 아니라 “완료돼야 할 작업 수”로 카운트를 잡을 수 있습니다.

메모리 가시성도 보장됩니다. count_down() 이전에 워커가 쓴 데이터는 wait()가 반환된 뒤 메인 스레드에서 안전하게 읽을 수 있습니다(표준이 count_down과 wait 사이에 synchronizes-with 관계를 정합니다). 따라서 워커가 결과를 각자의 벡터 슬롯에 쓰고 latch로 완료를 알리는 패턴에서는 결과 배열에 별도 mutex가 필요 없습니다. 다만 여러 워커가 같은 변수에 동시에 쓰는 것은 여전히 데이터 레이스이므로, 아래 테스트 러너 예제처럼 공유 카운터에는 mutex나 atomic이 필요합니다.

arrive_and_wait

#include <latch>
#include <thread>
#include <iostream>
int main() {
    std::latch done(3);
    
    auto worker = [&done](int id) {
        std::cout << "워커 " << id << " 시작" << std::endl;
        
        // count_down + wait
        done.arrive_and_wait();
        
        std::cout << "워커 " << id << " 재개" << std::endl;
    };
    
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

std::barrier - 반복 동기화

시그니처:

template<class CompletionFunction = /* see below */>
class barrier {
public:
    explicit barrier(ptrdiff_t expected, CompletionFunction f = {});
    void arrive_and_wait();
    void arrive_and_drop();
};

기본 사용

#include <barrier>
#include <thread>
#include <iostream>
void processData(std::barrier<>& sync, int id) {
    // 단계 1: 데이터 로드
    std::cout << id << ": 로드" << std::endl;
    sync.arrive_and_wait();
    
    // 단계 2: 처리
    std::cout << id << ": 처리" << std::endl;
    sync.arrive_and_wait();
    
    // 단계 3: 저장
    std::cout << id << ": 저장" << std::endl;
    sync.arrive_and_wait();
}
int main() {
    std::barrier sync(3);
    
    std::thread t1(processData, std::ref(sync), 1);
    std::thread t2(processData, std::ref(sync), 2);
    std::thread t3(processData, std::ref(sync), 3);
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

출력:

1: 로드
2: 로드
3: 로드
1: 처리
2: 처리
3: 처리
1: 저장
2: 저장
3: 저장

출력에서 보장되는 것은 단계 사이의 순서뿐입니다. 모든 “로드”가 모든 “처리”보다 먼저 나오지만, 같은 단계 안에서 1, 2, 3의 순서는 실행할 때마다 달라질 수 있고, std::cout에 여러 스레드가 동시에 쓰면 줄이 섞여 출력될 수도 있습니다(C++20 std::osyncstream을 쓰면 줄 단위로 묶을 수 있습니다).

barrier가 latch와 다른 점은 단계(phase)가 끝나면 카운트가 자동으로 원래 값으로 돌아간다는 것입니다. 세 번째 스레드가 도착하는 순간 한 단계가 완료되고, 완료 함수가 실행된 뒤 모든 대기 스레드가 풀려나며 카운트가 다시 3이 됩니다. 이 때문에 barrier는 모든 참여 스레드가 매 단계마다 반드시 한 번씩 도착해야 합니다. 한 스레드가 조건에 따라 어떤 단계를 건너뛰면 그 단계는 영원히 완료되지 않습니다.

완료 콜백

#include <barrier>
#include <thread>
#include <iostream>
int main() {
    int phase = 0;
    
    auto onCompletion = [&phase]() noexcept {
        std::cout << "단계 " << ++phase << " 완료" << std::endl;
    };
    
    std::barrier sync(3, onCompletion);
    
    auto worker = [&sync](int id) {
        for (int i = 0; i < 3; ++i) {
            std::cout << "워커 " << id << " 작업 " << i << std::endl;
            sync.arrive_and_wait();
        }
    };
    
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

arrive_and_drop

#include <barrier>
#include <thread>
#include <iostream>
void worker(std::barrier<>& sync, int id) {
    if (id == 0) {
        std::cout << "워커 0: 초기화 후 탈퇴" << std::endl;
        sync.arrive_and_drop();  // 카운트 감소 후 탈퇴
        return;
    }
    
    for (int i = 0; i < 3; ++i) {
        std::cout << "워커 " << id << " 작업 " << i << std::endl;
        sync.arrive_and_wait();
    }
}
int main() {
    std::barrier sync(5);
    
    std::thread t0(worker, std::ref(sync), 0);
    std::thread t1(worker, std::ref(sync), 1);
    std::thread t2(worker, std::ref(sync), 2);
    std::thread t3(worker, std::ref(sync), 3);
    std::thread t4(worker, std::ref(sync), 4);
    
    t0.join();
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    
    return 0;
}

arrive_and_drop()은 현재 단계에는 도착한 것으로 계산하면서, 다음 단계부터 기대 카운트를 1 줄입니다. 위 예제에서 barrier는 5로 시작하지만 워커 0이 탈퇴한 뒤부터는 4개 스레드만으로 단계가 완료됩니다. 작업이 일찍 끝난 스레드가 그냥 return해 버리면 남은 스레드가 영원히 기다리므로, 반복 도중 빠지는 스레드는 반드시 arrive_and_drop()을 호출하고 나가야 합니다. 반대로 참여 스레드를 늘리는 API는 없습니다. 스레드 수가 자주 바뀌는 구조라면 barrier보다 작업 큐와 condition_variable 쪽이 더 맞습니다.


고급 활용

병렬 초기화 패턴

#include <latch>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
class System {
private:
    std::latch initDone;
    
public:
    System(int numComponents) : initDone(numComponents) {}
    
    void initComponent(const std::string& name) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << name << " 초기화 완료" << std::endl;
        initDone.count_down();
    }
    
    void waitForInit() {
        initDone.wait();
        std::cout << "시스템 준비 완료" << std::endl;
    }
};
int main() {
    System system(3);
    
    std::thread t1(&System::initComponent, &system, "Database");
    std::thread t2(&System::initComponent, &system, "Cache");
    std::thread t3(&System::initComponent, &system, "Logger");
    
    system.waitForInit();
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

파이프라인 동기화

#include <barrier>
#include <thread>
#include <vector>
#include <iostream>
void pipelineWorker(std::barrier<>& sync, int id, int stages) {
    for (int stage = 0; stage < stages; ++stage) {
        std::cout << "워커 " << id << " 단계 " << stage << std::endl;
        sync.arrive_and_wait();
    }
}
int main() {
    const int numWorkers = 4;
    const int numStages = 3;
    
    std::barrier sync(numWorkers);
    
    std::vector<std::thread> threads;
    for (int i = 0; i < numWorkers; ++i) {
        threads.emplace_back(pipelineWorker, std::ref(sync), i, numStages);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

조건부 동기화

#include <latch>
#include <thread>
#include <vector>
#include <iostream>
#include <random>
int main() {
    std::latch done(5);
    
    auto worker = [&done](int id) {
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<> dis(0, 1);
        
        if (dis(gen) == 0) {
            std::cout << "워커 " << id << " 실패" << std::endl;
            done.count_down();  // 실패해도 카운트 감소
            return;
        }
        
        std::cout << "워커 " << id << " 성공" << std::endl;
        done.count_down();
    };
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(worker, i);
    }
    
    done.wait();
    std::cout << "모든 워커 완료 (성공/실패 무관)" << std::endl;
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

성능 비교

latch vs condition_variable

방식구현 방식코드 복잡도
condition_variablemutex + 카운터 + notify_all, spurious wakeup 대비 조건 재확인 필요높음
latch내부 원자 카운터, 표준 라이브러리 구현이 플랫폼 대기 기능(futex 등)을 활용낮음

latch의 가장 큰 이점은 속도보다 실수할 여지가 적다는 점입니다. condition_variable로 같은 동작을 만들면 mutex로 카운터를 보호하고, 조건을 루프로 다시 확인하고, 마지막 스레드가 notify_all을 불러야 합니다. 하나라도 빠뜨리면 교착이나 놓친 알림이 생깁니다. latch는 카운터 감소와 대기를 한 객체로 묶고, libstdc++·libc++ 같은 구현은 내부적으로 원자 연산과 OS 대기 기능을 써서 mutex 경합을 피하므로 보통 비슷하거나 더 가볍습니다.

barrier vs condition_variable

방식반복 사용코드 복잡도
condition_variable세대(generation) 카운터를 직접 관리해야 함높음
barrier단계가 끝나면 자동으로 초기화, 완료 콜백 지원낮음

반복 동기화를 condition_variable로 만들면 “이번 단계가 끝났는지”를 구분하는 세대 번호가 필요하고, 이를 잘못 다루면 빠른 스레드가 다음 단계로 넘어가 느린 스레드의 대기를 풀어 버리는 버그가 생깁니다. barrier는 이 세대 관리를 내부에서 처리합니다. 실제 대기 비용은 스레드 수와 구현에 따라 다르므로, 성능이 중요하다면 자기 워크로드로 측정해 보세요.


실무 사례

사례 1: 병렬 테스트 프레임워크

#include <latch>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
#include <mutex>
#include <string>
class TestRunner {
private:
    std::latch allTestsDone;
    int passedTests = 0;
    std::mutex resultMutex;
    
public:
    TestRunner(int numTests) : allTestsDone(numTests) {}
    
    void runTest(const std::string& testName, bool result) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        
        {
            std::lock_guard<std::mutex> lock(resultMutex);
            if (result) {
                passedTests++;
                std::cout << "[PASS] " << testName << std::endl;
            } else {
                std::cout << "[FAIL] " << testName << std::endl;
            }
        }
        
        allTestsDone.count_down();
    }
    
    void waitForResults() {
        allTestsDone.wait();
        std::cout << "\n테스트 완료: " << passedTests << " 통과" << std::endl;
    }
};
int main() {
    TestRunner runner(5);
    
    std::vector<std::thread> threads;
    threads.emplace_back(&TestRunner::runTest, &runner, "Test1", true);
    threads.emplace_back(&TestRunner::runTest, &runner, "Test2", true);
    threads.emplace_back(&TestRunner::runTest, &runner, "Test3", false);
    threads.emplace_back(&TestRunner::runTest, &runner, "Test4", true);
    threads.emplace_back(&TestRunner::runTest, &runner, "Test5", true);
    
    runner.waitForResults();
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

사례 2: 게임 엔진 - 프레임 동기화

#include <barrier>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
class GameEngine {
private:
    std::barrier<> frameSync;
    std::atomic<bool> running{true};  // 여러 스레드가 읽고 쓰므로 atomic (#include <atomic>)
    
public:
    GameEngine(int numSystems) : frameSync(numSystems) {}
    
    void physicsSystem() {
        while (running) {
            std::cout << "Physics 업데이트" << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(16));
            frameSync.arrive_and_wait();
        }
    }
    
    void renderSystem() {
        while (running) {
            std::cout << "Render 업데이트" << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(16));
            frameSync.arrive_and_wait();
        }
    }
    
    void audioSystem() {
        while (running) {
            std::cout << "Audio 업데이트" << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(16));
            frameSync.arrive_and_wait();
        }
    }
    
    void stop() {
        running = false;
    }
};
int main() {
    GameEngine engine(3);
    
    std::thread t1(&GameEngine::physicsSystem, &engine);
    std::thread t2(&GameEngine::renderSystem, &engine);
    std::thread t3(&GameEngine::audioSystem, &engine);
    
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    engine.stop();
    
    t1.join();
    t2.join();
    t3.join();
    
    return 0;
}

이 예제는 barrier를 쓸 때 가장 흔한 종료 버그를 그대로 담고 있습니다. stop()이 호출되는 순간 Physics 스레드는 이미 while (running) 검사를 통과해 다음 프레임을 시작했고, Render 스레드는 검사에서 false를 보고 루프를 빠져나갔다고 해 봅시다. 그러면 Physics는 arrive_and_wait()에서 Render를 기다리지만 Render는 다시 오지 않으므로 join()이 영원히 끝나지 않습니다. 실행해 보면 대부분 잘 끝나다가 가끔만 멈추는 형태라 재현이 어렵습니다(원래 코드처럼 running이 atomic이 아닌 bool이면 데이터 레이스로 미정의 동작까지 겹칩니다).

해결 방법은 모든 스레드가 같은 단계에서 같은 결정을 보게 만드는 것입니다. 완료 함수는 모든 스레드가 도착한 뒤, 누구도 풀려나기 전에 한 번만 실행되므로 종료 여부를 결정하기에 알맞은 장소입니다.

std::atomic<bool> stopRequested{false};
bool stopThisFrame = false;  // 완료 함수에서만 쓰고, barrier 이후에만 읽음

std::barrier frameSync(3, [&]() noexcept {
    stopThisFrame = stopRequested.load();
});

void systemLoop() {
    for (;;) {
        update();                    // 프레임 작업
        frameSync.arrive_and_wait(); // 완료 함수가 stopThisFrame 결정
        if (stopThisFrame) break;    // 세 스레드 모두 같은 값을 봄
    }
}

barrier는 완료 함수의 실행이 대기 해제보다 먼저 일어남(happens-before)을 보장하므로, stopThisFrame은 일반 bool이어도 모든 스레드가 같은 값을 읽습니다. 게임 루프, 시뮬레이션 스텝, 반복 수치 계산처럼 “N단계 반복 후 종료”하는 구조에서 barrier를 쓴다면 이 패턴을 기본으로 두는 것이 안전합니다.

사례 3: 데이터 처리 - 배치 작업

#include <barrier>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
void batchWorker(std::barrier<>& sync, int id, int batches) {
    for (int batch = 0; batch < batches; ++batch) {
        std::cout << "워커 " << id << " 배치 " << batch << " 처리" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        
        sync.arrive_and_wait();
    }
}
int main() {
    const int numWorkers = 4;
    const int numBatches = 3;
    
    auto onBatchComplete = []() noexcept {
        std::cout << "--- 배치 완료 ---" << std::endl;
    };
    
    std::barrier sync(numWorkers, onBatchComplete);
    
    std::vector<std::thread> threads;
    for (int i = 0; i < numWorkers; ++i) {
        threads.emplace_back(batchWorker, std::ref(sync), i, numBatches);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

트러블슈팅

문제 1: 카운트 불일치

증상: 영원히 대기 (데드락)

// ❌ 카운트 불일치
std::latch done(3);
std::thread t1([&]() { done.count_down(); });
std::thread t2([&]() { done.count_down(); });
// t3 없음
done.wait();  // 영원히 대기
t1.join();
t2.join();
// ✅ 올바른 카운트
std::latch done(2);  // 스레드 수와 일치
std::thread t1([&]() { done.count_down(); });
std::thread t2([&]() { done.count_down(); });
done.wait();  // OK
t1.join();
t2.join();

카운트 불일치는 대부분 “스레드 수”와 “latch 초기값”을 서로 다른 곳에서 관리할 때 생깁니다. 워커 수를 설정 파일에서 읽도록 바꾸면서 latch의 숫자 3은 그대로 두는 식입니다. std::latch done(workers.size())처럼 같은 값에서 파생시키면 이 부류의 실수는 거의 사라집니다. 반대로 카운트보다 더 많이 count_down()하는 것은 표준상 미정의 동작이라 예외도, 에러 메시지도 없이 이상하게 동작할 수 있습니다. 디버그 빌드에서 원인을 찾을 때는 wait() 대신 try_wait()를 주기적으로 호출하며 남은 작업을 로그로 찍어 보는 방법이 유용합니다. latch는 현재 카운트를 읽는 API가 없다는 점도 알아 두면 좋습니다.

문제 2: latch 재사용

증상: 재사용 불가

// ❌ latch 재사용
std::latch done(3);
done.count_down();
done.count_down();
done.count_down();
done.wait();
// done.count_down();  // 재사용 불가
// ✅ barrier 재사용
std::barrier sync(3);
sync.arrive_and_wait();
sync.arrive_and_wait();  // OK

문제 3: 예외 안전성

증상: 예외 발생 시 카운트 누락

#include <latch>
#include <thread>
#include <iostream>
// ❌ 예외 시 카운트 누락
void badWorker(std::latch& done) {
    // 작업
    throw std::runtime_error("에러");
    done.count_down();  // 실행 안 됨
}
// ✅ RAII 패턴
class LatchGuard {
private:
    std::latch& latch_;
    
public:
    explicit LatchGuard(std::latch& l) : latch_(l) {}
    ~LatchGuard() { latch_.count_down(); }
};
void goodWorker(std::latch& done) {
    LatchGuard guard(done);
    try {
        // 작업
        throw std::runtime_error("에러");
    } catch (const std::exception& e) {
        // 스레드 함수 밖으로 예외가 나가면 std::terminate가 호출되므로 여기서 처리
        std::cerr << "워커 예외: " << e.what() << '\n';
    }
    // 스코프를 벗어날 때 소멸자에서 count_down 호출
}
int main() {
    std::latch done(1);
    std::thread t(goodWorker, std::ref(done));
    done.wait();  // OK: 예외가 나도 카운트가 줄어듦
    t.join();
    return 0;
}

여기서 자주 오해하는 점이 하나 있습니다. std::thread에서 실행되는 함수가 예외를 밖으로 던지면 그 예외는 main의 try/catch로 전달되지 않고 즉시 std::terminate()가 호출되어 프로그램이 종료됩니다. 그래서 워커 안에서 예외를 잡아 처리하거나, std::exception_ptr에 담아(std::current_exception()) 공유 변수로 넘긴 뒤 wait()가 끝난 메인 스레드에서 std::rethrow_exception()으로 다시 던지는 방식을 씁니다. 스레드 대신 std::async를 쓰면 future.get()이 예외를 자동으로 전달해 주지만, 그 경우에도 latch 카운트는 위의 RAII 가드로 보장해야 합니다.

문제 4: barrier 카운트 변경 불가

증상: 동적으로 스레드 수 변경 불가

// ❌ 카운트 변경 불가
std::barrier sync(3);
// 스레드 추가하려면?
// sync.set_expected(4);  // 없음!
// ✅ 새로운 barrier 생성
std::barrier sync1(3);
// ... 사용 ...
std::barrier sync2(4);  // 새로운 barrier
// ... 사용 ...

마무리

C++20 std::latch와 std::barrier는 스레드 동기화를 간결하고 효율적으로 구현할 수 있게 합니다.

핵심 요약

  1. std::latch
    • 일회성 카운트다운
    • count_down(), wait()
    • 초기화 대기에 적합
  2. std::barrier
    • 반복 동기화
    • arrive_and_wait(), arrive_and_drop()
    • 단계별 처리에 적합
  3. 완료 콜백
    • barrier는 완료 함수 지원
    • 단계마다 자동 실행
  4. 성능과 안전성
    • 수동 mutex·카운터 관리 없이 원자 연산 기반으로 대기
    • 코드 간결성 향상

선택 가이드

상황도구
초기화 대기std::latch
단계별 동기화std::barrier
완료 콜백 필요std::barrier
동적 스레드 수condition_variable

코드 예제 치트시트

// latch: 일회성
std::latch done(3);
done.count_down();
done.wait();
// barrier: 반복
std::barrier sync(3);
sync.arrive_and_wait();
sync.arrive_and_wait();  // OK
// 완료 콜백
auto onComplete = []() noexcept { /* ... */ };
std::barrier sync(3, onComplete);
// 탈퇴
sync.arrive_and_drop();

다음 단계

참고 자료

  • “C++20 The Complete Guide” - Nicolai M. Josuttis
  • “C++ Concurrency in Action” - Anthony Williams
  • cppreference: https://en.cppreference.com/w/cpp/thread 한 줄 정리: latch는 일회성 동기화, barrier는 반복 동기화에 적합하며, condition_variable로 직접 구현할 때보다 실수할 여지가 적고 간결합니다.

자주 묻는 질문 (FAQ)

Q. 워커 스레드에서 예외가 나면 latch.wait()가 영원히 끝나지 않는 이유는 무엇인가요?

A. 예외가 count_down() 호출 전에 던져지면 그 스레드는 카운트를 줄이지 못하고, 카운트가 0에 도달하지 않으니 wait() 중인 스레드가 계속 기다리게 됩니다. 소멸자에서 count_down()을 호출하는 RAII 가드를 만들어 두면 정상 종료든 예외든 스코프를 벗어날 때 반드시 카운트가 줄어듭니다. 예외 자체는 별도로 저장하거나 전달해서 대기가 끝난 뒤 처리해야 원인이 묻히지 않습니다.


같이 보면 좋은 글