`std::stop_token`은 취소 전용 프리미티브가 아니라, 관찰자(Observer) 패턴을 구현하는 범용 1회성(one-shot) 신호 메커니즘이다. 이름과 1회성 제약이 활용을 가로막는다는 점을 짚고, 문서화·별칭·리셋 가능한 신호 시설을 제안한다.
URL: https://www.vinniefalco.com/p/recognizing-stop_token-as-a-general
이 글은 std::stop_token이 단지 취소(cancellation)를 위한 프리미티브가 아니라, 잘 확립된 관찰자(Observer) 패턴을 구현하는 범용 1회성(one-shot) 신호(signaling) 메커니즘이라고 주장한다. 그 기능은 스레드 취소를 훨씬 넘어선다.
작업 시작 또는 트리거
여러 관찰자에게 알림을 브로드캐스트
시스템 구성 요소로 미리 정의된 명령 전송
타입 소거(type-erased)된 다형적 콜백 등록 제공
이 패턴의 인지와 효용을 제한하는 문제는 두 가지다.
이름(Naming): “stop”이라는 이름이 더 넓은 사용 사례를 가린다. 사용자가 “C++ observer pattern”이나 “one-shot event”를 검색해도 stop_token을 발견하지 못한다.
1회성 제약(One-shot limitation): std::stop_token은 “신호되지 않음”에서 “신호됨”으로 단 한 번만 전이할 수 있다. 리셋(reset) 메커니즘이 없다. stop_requested()가 true를 반환하면, 그 토큰과 동일한 stop state를 공유하는 모든 복사본의 수명 동안 계속 true다. 이는 반복 신호가 필요한 사용 사례를 막는다.
우리는 문서 개선, 범용 이름의 타입 별칭, 그리고 이 설계의 잠재력을 온전히 실현하기 위한 리셋 가능한 신호 시설을 권고한다.
std::stop_token은 C++20(P0660R10)에서 도입되었고, 주된 동기는 std::jthread의 협력적 스레드 취소였다. 명명 역시 이 기원을 반영한다: stop_source, stop_token, stop_callback, request_stop(), stop_requested().
하지만 그 기반 메커니즘은 훨씬 더 일반적이다. stop_token 계열은 스레드 안전하고 타입 소거된 일대다(one-to-many) 알림 시스템을 구현한다. 이 패턴은 수십 년의 역사를 가지고 있으며 Observer, Signal-Slot, Event 같은 이름으로 알려져 있다.
이 글은 stop_token을 범용 기능의 관점에서 살펴보고, 더 넓은 채택을 막는 제한을 식별하며, 잠재력을 완전히 열기 위한 확장을 제안한다.
stop_token 계열은 여러 프로그래밍 언어와 프레임워크에서 다양한 이름으로 등장하는, 잘 알려진 설계 패턴을 구현한다.
관찰자(Observer) 패턴은 한 객체의 상태가 바뀔 때 그에 의존하는 모든 객체가 자동으로 통지받도록, 객체들 사이에 일대다 종속 관계를 정의한다. 이 패턴은 다음으로 구성된다.
주체(Subject): 관찰자 목록을 유지하고 상태 변화 시 통지
관찰자(Observer): 통지받아야 하는 객체를 위한 update 인터페이스 정의
std::stop_source가 Subject이고, std::stop_callback 인스턴스들이 Observer다.
Qt의 signal-slot 메커니즘은 객체들 사이에 타입 안전하고 느슨하게 결합된 통신을 제공한다. 주요 특징:
하나의 signal에 여러 slot을 연결 가능
연결은 런타임에 설정
signal을 emit하면 연결된 모든 slot이 호출됨
stop_token과 달리 Qt signals는 기본적으로 다회성(multi-shot)이다.
Boost.Signals2 라이브러리는 관리형 signals/slots의 C++ 구현을 제공하며 다음을 포함한다.
shared_ptr/weak_ptr를 통한 자동 연결 추적
스레드 안전한 signal 호출
사용자 정의 결과 결합기(result combiner)를 갖춘 멀티캐스트 지원
.NET은 여러 신호 메커니즘을 제공한다.
CancellationToken: 1회성으로, stop_token과 매우 유사하다. Microsoft 문서는 “CancellationToken은 본래 범위를 넘어, 애플리케이션 실행 상태에 대한 구독, 다양한 트리거를 이용한 작업 타임아웃, 그리고 플래그를 통한 일반적인 프로세스 간 통신까지 해결할 수 있다”고 인정한다.
ManualResetEvent: Set()과 Reset() 메서드를 가진 리셋 가능한 동기화 이벤트
AutoResetEvent: 하나의 대기 스레드를 해제한 뒤 자동으로 리셋
Google Chromium 프로젝트는 base::OneShotEvent를 제공하며 “한 번 발생할 것으로 기대되는 이벤트”라고 설명한다. 클라이언트는 이벤트가 신호된 뒤에 코드가 실행되도록 보장할 수 있다. 신호되기 전에 파괴되면 등록된 콜백은 실행되지 않은 채로 파괴된다.
이는 의미론적으로 stop_token과 동일하다.
stop_token 계열은 세 가지 협력 구성 요소로 이뤄지며, 함께 범용 신호 메커니즘을 구현한다.
std::stop_source는 공유 stop state를 소유하고 stop을 요청(신호 발생)할 수 있다.
class stop_source {
public:
stop_source();
explicit stop_source(nostopstate_t) noexcept;
stop_token get_token() const noexcept;
bool stop_possible() const noexcept;
bool stop_requested() const noexcept;
bool request_stop() noexcept; // Returns true on first call only
};
Observer 패턴 용어로는, 관찰자 상태를 유지하고 통지를 트리거하는 Subject다.
std::stop_token은 stop state에 대한 스레드 안전한 읽기 전용 뷰를 제공한다.
class stop_token {
public:
stop_token() noexcept;
bool stop_possible() const noexcept;
bool stop_requested() const noexcept;
};
여러 토큰이 같은 stop state를 공유할 수 있다. 이를 통해 통지 기능을 분배하되, 통지를 트리거할 권한은 부여하지 않을 수 있다.
std::stop_callback<Callback>은 연결된 stop_source가 신호될 때 호출될 콜백을 등록한다.
template<class Callback>
class stop_callback {
public:
template<class C>
explicit stop_callback(const stop_token& st, C&& cb);
~stop_callback(); // Unregisters callback
};
핵심 속성:
타입 소거(Type erasure): 각 stop_callback<F>는 서로 다른 호출 가능 타입 F를 저장할 수 있다.
RAII 의미론: 파괴 시 콜백 등록이 해제된다.
즉시 호출(Immediate invocation): stop이 이미 요청된 상태라면 생성자에서 콜백이 실행된다.
스레드 안전성(Thread safety): 콜백은 동기적으로 호출되지만, 등록은 스레드 안전하다.
이는 가상 함수나 관찰자당 힙 할당 없이 다형적 관찰자 목록을 유지하는 것과 같다.
stop_token 메커니즘은 취소와 무관한 많은 용도로 사용할 수 있다.
초기화를 트리거하는 “준비(ready)” 신호:
std::stop_source ready_signal;
// Workers register interest in the start signal
std::stop_callback worker1(ready_signal.get_token(), []{
initialize_subsystem_a();
});
std::stop_callback worker2(ready_signal.get_token(), []{
initialize_subsystem_b();
});
// Later: trigger initialization
ready_signal.request_stop(); // Name suggests “stopping”, but we’re starting
구성이 사용 가능해졌을 때 구성 요소에 알림:
std::stop_source config_ready;
// UI component
std::stop_callback ui_cb(config_ready.get_token(), [&]{
apply_theme(config.theme);
});
// Network component
std::stop_callback net_cb(config_ready.get_token(), [&]{
set_timeout(config.timeout);
});
// After config loads
config_ready.request_stop();
공유 리소스가 사용 가능해졌음을 신호:
std::stop_source db_connected;
std::stop_callback cache_init(db_connected.get_token(), [&]{
warm_cache_from_db();
});
std::stop_callback metrics_init(db_connected.get_token(), [&]{
start_metrics_collection();
});
// When database connection established
db_connected.request_stop();
stop_callback 메커니즘은 가상 함수 없이 타입 소거를 제공한다.
std::stop_source event;
// Different callable types coexist
std::stop_callback cb1(event.get_token(), []{ /* lambda */ });
std::stop_callback cb2(event.get_token(), std::bind(&Foo::bar, &foo));
std::stop_callback cb3(event.get_token(), my_functor{});
// stop_source doesn’t know the concrete types
// yet invokes all callbacks when signaled
event.request_stop();
이는 std::vector<std::function<void()>>를 유지하는 것과 동등하지만 다음 장점이 있다.
콜백마다 힙 할당이 없다(콜백은 스택에 배치)
RAII를 통한 자동 수명 관리
스레드 안전한 등록 및 호출
중요한 제약 하나가 stop_token이 완전한 범용 신호 메커니즘으로 동작하는 것을 막는다.
std::stop_source signal;
// First signal works
bool first = signal.request_stop(); // returns true, callbacks invoked
// Subsequent signals are no-ops
bool second = signal.request_stop(); // returns false, nothing happens
bool third = signal.request_stop(); // returns false, nothing happens
제약은 설계에 본질적이다.
stop_requested()는 false에서 true로만 전이하며, 되돌아갈 수 없다.
stop_source에 reset() 메서드가 없다.
동일 stop state를 공유하는 모든 토큰은 영구적으로 신호된 상태가 된다.
request_stop()은 첫 성공 호출에서만 true를 반환한다.
이 1회성 특성은 여러 흔한 신호 패턴을 막는다.
일시정지/재개(Pause/Resume): “pause”를 신호한 뒤 나중에 “resume”을 신호할 수 없다.
주기적 알림(Periodic notifications): 반복 이벤트(heartbeat, tick, frame)에 대해 관찰자에게 알릴 수 없다.
상태 기계(State machines): 하나의 메커니즘으로 여러 상태 전이를 신호할 수 없다.
리소스 풀(Resource pools): 리소스가 풀로 반환될 때마다 “available”을 반복 신호할 수 없다.
재시도 가능한 작업(Retriable operations): 일시적 실패 후 재시도를 허용하도록 상태를 리셋할 수 없다.
대부분의 플랫폼은 1회성 이벤트와 리셋 가능/다회성 이벤트를 구분한다.
.NET ManualResetEvent: Set()으로 신호하고 Reset()으로 해제
.NET AutoResetEvent: 하나의 대기 스레드를 해제한 뒤 자동 리셋
Win32 CreateEvent: ResetEvent()를 통해 수동 리셋/자동 리셋 모드 모두 지원
POSIX pthread_cond_t: 조건 변수는 본질적으로 다회성
Qt signals: 기본적으로 다회성; 반복적으로 emit 가능
Boost.Signals2: 다회성; 어떤 횟수로든 호출 가능
C++는 현재 리셋 가능한 대안 없이 1회성 변형만 제공한다.
우리는 리셋 가능한 신호 시설의 도입을 제안한다.
namespace std {
class signal_source {
public:
signal_source();
explicit signal_source(nosignalstate_t) noexcept;
~signal_source();
signal_source(const signal_source&) = delete;
signal_source& operator=(const signal_source&) = delete;
signal_token get_token() const noexcept;
bool signal() noexcept; // Set to signaled, invoke callbacks
void reset() noexcept; // Return to non-signaled state
bool is_signaled() const noexcept;
bool signal_possible() const noexcept;
};
class signal_token {
public:
signal_token() noexcept;
bool is_signaled() const noexcept;
bool signal_possible() const noexcept;
};
template<class Callback>
class signal_callback {
public:
template<class C>
explicit signal_callback(const signal_token& st, C&& cb);
~signal_callback();
};
}
핵심 추가점은 reset()으로, 신호를 “비신호” 상태로 되돌려 재사용을 가능하게 한다.
“stop” 용어는 stop_token의 범용성을 인식하는 데 적극적으로 방해가 된다.
사용자가 표준 해법을 찾으려 검색해도 stop_token을 발견하지 못한다.
“C++ observer pattern” — stop_token 언급 없음
“C++ one-shot event” — stop_token 언급 없음
“C++ broadcast notification” — stop_token 언급 없음
“C++ signal callback” — POSIX signal 또는 Boost.Signals2로 유도됨
API 명명은 범용 신호에 적용되지 않는 취소 의미론을 암시한다.
// Semantically: “signal that initialization is complete”
// API says: “request stop”
init_signal.request_stop();
// Semantically: “check if ready”
// API says: “check if stop requested”
if (ready_signal.get_token().stop_requested()) { ... }
일반성을 더 잘 드러내는 이름:
signal_source / signal_token / signal_callback — Qt/Boost 용어와 부합
event_source / event_token / event_callback — .NET/Win32 용어와 부합
notification_source / notification_token / notification_callback — 설명적
one_shot_event — Chromium의 명명과 부합, 제약을 명시
언어와 프레임워크 전반의 신호 메커니즘 조사:
.NET CancellationToken: 1회성, stop_token과 유사. 문서가 범용 사용을 인정.
.NET ManualResetEvent: Reset()로 리셋 가능. “Event”로 명명.
.NET AutoResetEvent: 각 신호 후 자동 리셋. “Event”로 명명.
Win32 CreateEvent: ResetEvent()로 리셋 가능. “Event”로 명명.
Qt QObject signals: 다회성, 반복 emit 가능. “Signal”로 명명.
Chromium base::OneShotEvent: 콜백을 갖는 1회성. “Event”로 명명.
Boost.Signals2: 연결 관리를 갖는 다회성. “Signal”로 명명.
Java PropertyChangeListener: 다회성 관찰자 패턴. “Listener”로 명명.
C++ std::stop_token: 1회성, 리셋 없음. “Stop”으로 명명.
관찰:
대부분의 플랫폼은 “signal” 또는 “event” 용어를 사용한다.
대부분의 플랫폼은 1회성과 다회성/리셋 가능 변형을 모두 제공한다.
C++는 “stop” 용어를 사용하는 점에서 독특하다.
C++는 1회성 변형만 제공한다.
표준과 cppreference에 stop_token의 범용 신호 사용 사례를 인정하는 비규범(non-normative) 노트를 추가하자.
[Note: stop_token은 협력적 취소를 위해 설계되었지만, 스레드 안전한 일대다 통지 메커니즘은 초기화 신호, 리소스 가용성 알림, 명령 디스패치 등 어떤 1회성 신호 시나리오에도 적합하다. —end note]
튜토리얼과 교육 자료에서 stop_token을 먼저 신호 프리미티브로 소개하고, 취소는 그 특정 응용 중 하나로 제시하도록 장려하자.
범용 이름을 가진 타입 별칭을 도입하자.
namespace std {
using one_shot_signal_source = stop_source;
using one_shot_signal_token = stop_token;
template<class Callback>
using one_shot_signal_callback = stop_callback<Callback>;
}
이는 기존 코드를 깨뜨리거나 구현 부담을 추가하지 않고도 발견 가능성을 개선한다.
다음 특성을 갖는 새로운 signal_source/signal_token/signal_callback 계열을 제안한다.
Observer 패턴을 반영하는 범용 명명
reset() 메서드를 통한 리셋 가능한 의미론
기존 stop_token 개념(stoppable_token 등)과의 호환성
namespace std {
// Resettable multi-shot signal
class signal_source {
public:
signal_token get_token() const noexcept;
bool signal() noexcept; // Returns true if state changed
void reset() noexcept; // Return to non-signaled state
bool is_signaled() const noexcept;
};
// One-shot signal (better-named stop_token equivalent)
class one_shot_signal_source {
public:
one_shot_signal_token get_token() const noexcept;
bool signal() noexcept; // Effective only once
bool is_signaled() const noexcept;
};
}
std::stop_token은 Observer 패턴을 구현한다. Observer 패턴은 수십 년에 걸쳐 다양한 언어에서 유용성이 검증된 근본적인 설계 패턴이다. stop_token의 역량은 스레드 취소를 넘어, 범용 신호, 알림 브로드캐스트, 타입 소거 기반 콜백 관리로 확장된다.
그러나 사용자가 이 패턴을 인식하고 완전히 활용하는 데는 두 가지 제한이 있다.
이름: “stop” 용어가 일반 적용 가능성을 가린다.
1회성 제약: 리셋 메커니즘의 부재가 사용 사례를 제한한다.
권고 사항:
범용 신호 사용을 인정하는 문서화
범용 이름의 타입 별칭
새로운 리셋 가능한 신호 시설
이러한 변경은 C++ 사용자가 이 강력한 패턴을 발견하고 적용하는 데 도움이 되며, C++를 다른 언어와 프레임워크의 확립된 관행과 더 잘 맞추게 할 것이다.
P0660의 저자들이 이 유용한 메커니즘을 C++20에 도입한 것에 감사한다(비록 범용 유틸리티가 주된 동기가 아니었더라도).
[P0660R10] Nicolai Josuttis, Lewis Baker, Billy O’Neal, Herb Sutter. Stop Token and Joining Thread. https://wg21.link/P0660R10
[P2175R0] Kirk Shoop, Lee Howes, Lewis Baker. Composable cancellation for sender-based async operations. https://wg21.link/P2175R0
[GoF] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
[Qt Signals] Qt Documentation. Signals & Slots. https://doc.qt.io/qt-6/signalsandslots.html
[Boost.Signals2] Frank Mori Hess. Boost.Signals2. https://www.boost.org/doc/libs/release/doc/html/signals2.html
[ManualResetEvent] Microsoft. ManualResetEvent Class. https://learn.microsoft.com/en-us/dotnet/api/system.threading.manualresetevent
[CancellationToken] Microsoft. Recommended patterns for CancellationToken. https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/
[OneShotEvent] Chromium. base/one_shot_event.h. https://chromium.googlesource.com/chromium/src/+/main/base/one_shot_event.h
stop_token을 범용 신호 메커니즘으로 제시하고, 명명 개선과 리셋 가능한 변형을 제안하는 초기 개정본.