센더/리시버 기초 문법 — sender, then, when_all
가장 단순한 센더 — just
앞 장에서 본 대로 ex::just(value)는 값을 즉시 담은 센더를 만듭니다. 아무것도 실행하지 않은 상태의 "작업 설명서"라고 생각하면 됩니다.
#include <execution>
#include <iostream>
namespace ex = std::execution;
ex::sender auto s = ex::just(10);이 상태로는 10이 어디에도 쓰이지 않습니다. 실행 결과를 실제로 얻으려면 sync_wait로 현재 스레드를 블로킹하며 결과를 기다려야 합니다.
auto result = ex::sync_wait(s); // std::optional<std::tuple<int>>
if (result) {
auto [value] = *result;
std::cout << value << "\n"; // 10
}sync_wait가 std::optional<std::tuple<...>>를 반환하는 이유가 있습니다. 센더는 성공(set_value) 말고도 취소(set_stopped)로 끝날 수 있는데, 취소된 경우 sync_wait는 값이 없는 std::nullopt을 돌려줍니다. 예외로 끝난 경우는 sync_wait 자체가 예외를 다시 던집니다.
then — 센더 위에 변환을 얹기
가장 자주 쓰게 될 조합기는 then입니다. 이전 센더의 결과를 받아 다른 값으로 바꿉니다.
ex::sender auto pipeline =
ex::just(3)
| ex::then([](int x) { return x * 2; })
| ex::then([](int x) { return x + 1; });
auto [result] = *ex::sync_wait(pipeline); // 7| 연산자로 체이닝한다는 점이 눈에 띌 겁니다. 이건 우연이 아니라 C++20 레인지(std::views::filter | std::views::transform)와 일부러 맞춘 관례입니다. "값을 변환하며 흘려보낸다"는 정신이 레인지와 센더가 같기 때문에, 위원회가 의도적으로 문법을 통일했습니다.
when_all — 여러 비동기 작업을 동시에 기다리기
콜백 기반 코드에서 "두 요청을 동시에 보내고 둘 다 끝나면 다음으로"를 표현하려면 카운터나 플래그를 손으로 관리해야 했습니다. 센더는 이걸 조합기 하나로 표현합니다.
ex::sender auto fetch_user = ex::just(1) | ex::then([](int id) {
return std::string("user-") + std::to_string(id);
});
ex::sender auto fetch_settings = ex::just(1) | ex::then([](int id) {
return std::string("settings-") + std::to_string(id);
});
ex::sender auto combined = ex::when_all(fetch_user, fetch_settings)
| ex::then([](std::string user, std::string settings) {
return user + " / " + settings;
});
auto [result] = *ex::sync_wait(combined);
// "user-1 / settings-1"when_all이 넘겨받은 센더들은 병렬로 실행될 수 있습니다("될 수 있다"인 이유는 실제 병렬성이 그 센더가 어느 스케줄러에 붙어 있는지에 달렸기 때문입니다). 이후 then으로 넘어가는 람다는 각 센더의 결과를 그대로 인자로 받습니다 — 여기서는 user, settings 두 개입니다.
scheduler — 어디서 실행할지 명시하기
지금까지의 예제는 전부 sync_wait를 부른 스레드에서 그대로 실행됐습니다. 실제로 스레드 풀에서 작업을 돌리려면 스케줄러를 지정해야 합니다.
#include <exec/static_thread_pool.hpp> // NVIDIA stdexec 참조 구현 예시
exec::static_thread_pool pool(4);
auto sched = pool.get_scheduler();
ex::sender auto work =
ex::schedule(sched) // 이 스케줄러에서 시작하고
| ex::then([] { return heavy_computation(); })
| ex::then([](int r) { return r + 1; });
auto [result] = *ex::sync_wait(work);ex::schedule(sched)가 체인의 시작점을 그 스케줄러로 못박습니다. 이 한 줄만 봐도 이 작업이 어느 실행 컨텍스트에서 도는지 코드에 드러난다는 게, 앞 장에서 지적한 콜백 코드의 세 번째 문제(실행 컨텍스트가 안 보이는 문제)를 해결하는 지점입니다.
여기 쓴 헤더와 네임스페이스는 P2300 표준 문법과, 현재 실무에서 가장 널리 쓰이는 선행 구현체인 NVIDIA의 stdexec를 함께 참고했습니다. 표준 라이브러리에 <execution>이 완전히 자리 잡기 전까지는 stdexec로 이 모델을 미리 연습해볼 수 있습니다. 다음 장에서는 이 문법들을 엮어 실제 시나리오 하나를 끝까지 짜보겠습니다.