📌 연관 기술 포스트:
C++26의 최신 병렬/비동기 아키텍처 및 HTS 데이터 파이프라인 시리즈와 연결됩니다.
👉 [C++] C++26 계약(Contracts)과 정적 리플렉션: HTS 및 고성능 트레이딩 시스템의 패러다임 변화
👉 [C++] HTS 실시간 시세 처리 심화: 다중 PB 서비스 수신과 종목별 병렬 처리

C++에서 비동기 및 병렬 처리는 오랫동안 개발자들을 괴롭혀온 난제였습니다. std::thread, std::future, std::promise는 힙 메모리 할당 강제, 비동기 작업의 합성(Composition) 불가, 스레드 블로킹, 취소(Cancellation) 메커니즘의 부재라는 한계를 안고 있었습니다.

C++26에 공식 도입된 std::execution (P2300: Senders/Receivers 모델)은 이러한 파편화된 비동기 생태계를 완전히 통합하고, 동적 할당 없는(Zero-Allocation) 구조화된 동시성(Structured Concurrency)을 언어 표준으로 제공합니다.

본 글에서는 std::execution의 핵심 3대 개념(Schedulers, Senders, Receivers)과 동작 원리를 살펴보고, 고성능 HTS 및 서버 환경에서 비동기 파이프라인을 구축하는 구체적인 실무 코드 예시를 상세히 분석합니다.


1. 기존 C++ 동시성 모델의 한계와 std::execution의 등장 배경

기존 C++ 표준이 제공하던 비동기 도구들은 현대의 멀티코어·초저지연 시스템 요구사항을 충족하기에 역부족이었습니다:

  • std::future / std::promise의 병목: 결과를 공유하기 위해 내부적으로 무조건 힙 메모리(Shared State)를 동적 할당하며, 여러 비동기 작업을 이어 붙이는 체이닝(Chaining, .then())이 표준에 없어 스레드가 get()에서 블로킹되어야 했습니다.
  • std::async의 결함: 반환된 future의 소멸자가 암묵적으로 블로킹을 유발하여 예측 불가능한 지연을 초래했습니다.
  • 취소(Cancellation) 지원 부재: 비동기 작업 도중 타임아웃이나 사용자 취소 요청이 발생했을 때 이를 안전하게 중단할 표준적인 경로가 없었습니다.

std::execution은 "작업의 정의(What)"와 "작업의 실행 장소(Where/How)"를 완전히 분리하여, 힙 할당 없이 컴파일 타임에 모든 비동기 작업 그래프를 합성하고 최적화할 수 있도록 설계되었습니다.

기존 std::future 모델 vs C++26 std::execution Senders/Receivers 아키텍처 비교
▲ 비동기 모델의 진화: Future vs C++26 std::execution

2. std::execution의 3대 핵심 추상화: Schedulers, Senders, Receivers

std::execution 모델은 직관적이고 강력한 3가지 핵심 요소로 구성됩니다:

2.1 Schedulers (스케줄러)

  • 역할: 작업이 실행될 실행 컨텍스트(스레드 풀, GPU, 이벤트 루프, I/O 스레드 등)를 나타내는 경량 핸들입니다.
  • 동작: std::execution::schedule(sch) 함수를 호출하면, 해당 스케줄러 위에서 시작될 준비가 된 Sender를 반환합니다.

2.2 Senders (송신자)

  • 역할: 비동기 작업의 '지연된 레시피(Lazy Recipe)'입니다. 즉, 생성 즉시 실행되지 않으며 비동기 연산의 청사진을 기술합니다.
  • 3가지 완료 신호(Completion Signals):
    1. set_value(args...): 작업이 정상 완료되어 결과값을 전달
    2. set_error(e): 작업 중 예외나 에러가 발생하여 에러를 전달
    3. set_stopped(): 작업이 취소되어 안전하게 중단됨

2.3 Receivers (수신자)

  • 역할: Sender가 방출한 3가지 신호를 전달받아 소비하는 콜백 인터페이스입니다.
  • 개발자가 직접 Receiver를 구현할 일은 거의 없으며, 표준 알고리즘이 내부적으로 Sender와 Receiver를 결합(connect)하여 실행 상태(operation_state)를 생성합니다.
Senders, Receivers, Schedulers 3대 핵심 추상화 및 신호 생명주기
▲ Senders, Receivers, Schedulers 3대 핵심 추상화와 신호 흐름

3. 고성능 비동기 파이프라인 구축 및 핵심 알고리즘

std::execution은 C++20의 파이프 연산자(|)를 지원하여 복잡한 비동기 흐름을 마치 유닉스 파이프라인처럼 선언형으로 합성할 수 있습니다.

3.1 실무 기초 파이프라인 예시: 스레드 전환과 체이닝

I/O 스레드 풀에서 데이터를 수신하고, 계산 스레드 풀로 컨텍스트를 전환(Transfer)하여 가공한 뒤 결과를 동기식으로 수신하는 기본 예제입니다.

#include <execution>
#include <iostream>

namespace ex = std::execution;

void BasicPipelineExample(ex::scheduler auto io_sched, ex::scheduler auto cpu_sched) {
    // 비동기 작업 파이프라인 선언 (아직 실행되지 않는 Lazy Sender)
    auto work = ex::schedule(io_sched)
        // 1. I/O 스케줄러에서 데이터 읽기
        | ex::then([]() {
            std::cout << "[I/O Thread] Raw data received\n";
            return 42; // 원시 데이터 반환
        })
        // 2. CPU 계산 스케줄러(스레드 풀)로 컨텍스트 전환
        | ex::transfer(cpu_sched)
        // 3. CPU 스레드에서 무거운 연산 수행
        | ex::then([](int val) {
            std::cout << "[CPU Thread] Processing heavy computation\n";
            return val * 100;
        })
        // 4. 에러 처리 핸들러 부착
        | ex::upon_error([](std::exception_ptr e) {
            std::cerr << "Error occurred during execution\n";
            return 0;
        });

    // 5. 파이프라인 시작 및 동기 대기 (sync_wait)
    auto result = ex::sync_wait(std::move(work));
    if (result) {
        auto [finalValue] = *result;
        std::cout << "Final Result: " << finalValue << "\n";
    }
}

4. 실전 응용: HTS 실시간 시세 처리 파이프라인

금융 HTS 시스템에서 네트워크 패킷 수신부터 이동평균 계산, UI 통지까지의 실시간 파이프라인을 std::execution으로 완벽히 구조화한 실무 코드입니다.

#include <execution>
#include <tuple>
#include <vector>

namespace ex = std::execution;

struct MarketTick {
    uint32_t symbolId;
    double price;
    uint64_t volume;
};

struct AnalyzedIndicator {
    uint32_t symbolId;
    double movingAverage;
    bool isBreakout;
};

// 실시간 시세 처리 파이프라인 생성 함수
auto CreateMarketDataPipeline(ex::scheduler auto netSched, 
                             ex::scheduler auto workerPool, 
                             ex::scheduler auto uiSched,
                             uint32_t targetSymbol) 
{
    return ex::schedule(netSched)
        // 1. 네트워크 컨텍스트에서 시세 패킷 수신
        | ex::then([targetSymbol]() -> MarketTick {
            return NetworkDriver::FetchTick(targetSymbol);
        })
        // 2. 고성능 워커 스레드 풀로 전환하여 기술적 지표 병렬 계산
        | ex::transfer(workerPool)
        | ex::then([](const MarketTick& tick) -> AnalyzedIndicator {
            double ma20 = IndicatorEngine::CalculateMA(tick.symbolId, tick.price);
            bool breakout = tick.price > ma20 * 1.05;
            return AnalyzedIndicator{ tick.symbolId, ma20, breakout };
        })
        // 3. UI 스레드로 안전하게 전환하여 렌더링 갱신
        | ex::transfer(uiSched)
        | ex::then([](const AnalyzedIndicator& indicator) {
            if (indicator.isBreakout) {
                UIComponent::HighlightSignal(indicator.symbolId);
            }
            UIComponent::UpdatePriceView(indicator.symbolId, indicator.movingAverage);
        });
}

4.1 when_all을 활용한 다중 종목 병렬 수집 및 조인

여러 종목의 시세를 병렬로 동시에 가져와 단일 지표로 결합할 때는 when_all을 사용합니다.

auto multiFetch = ex::when_all(
    CreateMarketDataPipeline(netSched, workerPool, uiSched, 005930), // 삼성전자
    CreateMarketDataPipeline(netSched, workerPool, uiSched, 000660)  // SK하이닉스
)
| ex::then([](auto res1, auto res2) {
    std::cout << "Both tickers processed in parallel without lock contention!\n";
});
HTS 실시간 시세 멀티 스레드 파이프라인 (Network -> Worker Pool -> UI Sched)
▲ HTS 실시간 시세 처리 비동기 파이프라인 흐름도

5. 기존 모델과의 종합 비교

비교 항목 std::thread / std::future C++20 Coroutines C++26 std::execution
메모리 할당 Shared State 힙 할당 강제 코루틴 프레임 힙 할당 (HALO 미보장 시) 스택 기반 0-Allocation (인라인 합성)
작업 합성성 거의 불가능 (블로킹 필요) co_await 순차 합성 우수 파이프 연산자(|) 및 when_all 병렬 합성 극대화
취소(Cancellation) 표준 지원 없음 별도 라이브러리 필요 언어 표준 set_stopped 취소 전파
스케줄러 추상화 없음 (OS 스레드 종속) 커스텀 Awaiter 필요 CPU/GPU/io_uring 범용 Scheduler 인터페이스
캐시 친화도 낮음 (포인터 인디렉션) 보통 최상 (컴파일 타임 정적 구조체 평탄화)

6. 결론: C++ 비동기 프로그래밍의 표준 완성

std::execution (P2300)은 C++ 언어에 마침내 완성된 진정한 의미의 비동기 프레임워크를 제공합니다:

✨ std::execution의 3대 핵심 강점:
  • 무비용 추상화(Zero-Cost Abstraction): 힙 메모리 할당 없이 모든 비동기 상태를 스택 상의 템플릿 타입으로 평탄화하여 극한의 처리량을 보장합니다.
  • 구조화된 동시성(Structured Concurrency): 비동기 작업의 시작, 완료, 에러, 취소가 단일 파이프라인 수명주기 내에서 완벽히 통제됩니다.
  • 하드웨어 독립성: 동일한 비동기 알고리즘을 단 한 줄의 수정으로 CPU 스레드 풀, GPU 커널, 또는 io_uring 비동기 I/O에서 구동할 수 있습니다.

현재 NVIDIA의 오픈소스 레퍼런스 구현체인 stdexec를 통해 C++20/C++23 환경에서도 즉시 이 모델을 도입할 수 있으므로, 고성능 분산 시스템 및 트레이딩 인프라를 구축하는 팀이라면 필수적으로 검토해야 할 표준 아키텍처입니다.


7. 자주 묻는 질문 (FAQ)

Q1. std::execution과 C++20 코루틴(Coroutines)은 경쟁 관계인가요?
아닙니다. 두 기술은 상호 보완적입니다. std::execution의 Sender는 코루틴 내에서 co_await sender 형태로 자연스럽게 대기할 수 있으며, 코루틴의 가독성과 Senders/Receivers의 제로 오버헤드 병렬 합성 성능을 결합하여 사용할 수 있습니다.
Q2. std::execution에서 동적 메모리 할당이 전혀 발생하지 않는 이유는 무엇인가요?
모든 비동기 체이닝(then, transfer 등)이 템플릿 래퍼 타입으로 컴파일 타임에 중첩 정의되기 때문입니다. connect 호출 시 전체 실행 상태(Operation State)가 단일 스택 객체로 결정되므로 런타임 힙 할당이 전혀 발생하지 않습니다.
Q3. C++26 이전 버전(C++20/C++23)에서 바로 사용해 볼 수 있는 방법이 있나요?
NVIDIA에서 공식 유지보수 중인 헤더 온리 라이브러리 stdexec (github.com/NVIDIA/stdexec)를 사용하면 C++20 이상의 표준 컴파일러(GCC 11+, Clang 13+, MSVC 2022+)에서 동일한 P2300 스펙의 Senders/Receivers 기능을 즉시 활용할 수 있습니다.