Async Programming with C++: Global vs SPI Callbacks. What's the difference?
6 Aug 2026 · 16 views

Asynchronous Programming Models in C++: A Practical Overview
Intro
One long-standing criticism of C++ is that its standard library offers very limited support for asynchrony, providing only a handful of async primitives. Although modern standards have improved things somewhat, these features see little use in practice, so developers really ought to take the initiative to learn the underlying mechanics of asynchronous programming themselves. This article gives a practical overview of the most common async models.
Async vs. Sync
Synchronous programming is the paradigm everyone is most familiar with. Setting aside compiler and CPU reordering for a moment: whatever order you call functions in your code is the order in which they get called at runtime. In other words, the developer invokes these functions directly — the developer never hands the right to call (a function pointer) over to some other mechanism.
In asynchronous programming, an async function is called a callback. The core idea behind the name is: "I want this function I wrote to be invoked by a specific component under specific conditions, possibly with arguments that the component decides." The developer only needs to define the function and hand over the pointer. "Callback" literally means: once some component has the data ready, it calls this function back.
A classic example of async processing is market data. A user can synchronously send a subscription request, but processing that request takes time on the broker's servers. The broker knows when its data is ready — you don't. So naturally, you need to hand a callback function to the broker's SDK and let the SDK invoke your function every time a market data update arrives. You've probably seen functions like this:
void OnTick(TICK_MSG tick){
MY_TICK tick_data;
tick_data.price = tick.price() / 10000.0;
tick_side = convert_side(tick.side());
// ...
ring_buffer.push(tick);
}
This is a textbook callback. The broker's SDK calls it once the data is ready, passing in the prepared TICK_MSG as an argument. The function itself is defined by the user, and the user decides what to do with that argument — which is exactly how the user ends up receiving the data they wanted.
Of course, not every market data API is asynchronous. Some brokers support both synchronous data queries (e.g., orders, trade records) and asynchronous data pushes.
Global Callbacks
How exactly you implement "hand the right to call a function over to another component, and have it invoked once things are ready" is what defines an async model. The most bare-bones approach — and the one commonly used in C/C++ — is the global callback. The core idea is to pass a function pointer directly to the SDK's callback registration function; the SDK then internally handles "call this function once the event is ready." Let's look at an example. Suppose the SDK exposes the following interface:
// Callback func decl
typedef void (*OnTickCallback)(const TickData* tick, void* user_data);
// Callback registration func decl
void RegisterCallback(OnTickCallback cb, void* user_data);
On the developer's side, you register a callback like this:
// Customized callback func
void MyOnTick(const TickData* tick, void* ud) {
auto* ctx = static_cast<MyContext*>(ud);
}
// Register callback func
RegisterCallback(MyOnTick, &my_ctx);
This style of callback requires no sophisticated language features at all — it can be implemented in plain C, is ABI-compatible, and works well across languages and compilers. In fact, most SDKs are implemented this way under the hood; the SPI model discussed next is merely a wrapper on top of it. Note that the function you pass in must be static — a function with no context. (A member function, for example, is a function with context, so it can't be passed.) This is quite intuitive: the caller is the SDK, not your class or whatever scope you defined, so any context must be passed in explicitly as an argument.
SPI Callbacks
SPI stands for Service Provider Interface. The core idea: the SDK defines an abstract base class where each event type gets its own virtual function. You inherit from it, implement the functions you care about, and pass the instantiated object to the SDK's object registration function. The difference from global callbacks is that SPI registers a defined object, whereas a global callback registers a defined function pointer. Here's a small example:
// SDK provides:
class CThostFtdcMdSpi {
public:
virtual void OnFrontConnected() {}
virtual void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* p) {}
virtual void OnRspError(...) {}
};
// Customized override
class MyMdSpi : public CThostFtdcMdSpi {
void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* p) override {
// `this` is naturally available; state lives in member variables
}
};
api->RegisterSpi(&my_spi); // Register an object
The strength of SPI is that it's naturally OOP: it integrates into the structure of most C++ programs and enjoys all the usual OOP benefits — state can live inside the object without being passed around explicitly. Events are neatly categorized as virtual functions, and developers only need to override the ones they need. However, its underlying implementation — such as vtable layout — may depend on the C++ ABI, which means compatibility issues across compilers. I actually ran into this myself while integrating an A-share market data feed. We had no way to fix it on our end and could only ask the broker for a GCC 7 build of the SDK.
Back to C++
Making developers handle these async matters themselves isn't purely a bad thing. Precisely because developers retain broader control, more performance optimizations become possible. Many market data SDKs ship performance-oriented functions — thread-to-core pinning, sleep/spin strategies, etc. In languages where async functionality is heavily abstracted away, such optimizations are rare. It always comes down to the specific business case, and for a demanding domain like quantitative trading, C++ is the better fit. Not to mention that the SDKs provided by A-share brokers are all C++ anyway.