-
-
Notifications
You must be signed in to change notification settings - Fork 12
async_io.md
The QB framework's ability to handle numerous concurrent operations efficiently, especially network and file interactions, stems from its powerful asynchronous I/O model. This model, provided by the qb-io library, ensures that your application remains responsive and scalable.
Traditional I/O operations are often blocking: when your code tries to read from a network socket or a file, it pauses (blocks) until the data is available or the operation completes. In a high-concurrency environment, this leads to idle threads and wasted CPU cycles.
QB champions a non-blocking approach:
- Initiate Operation: Your code requests an I/O operation (e.g., start reading from a socket).
-
Register Interest: The QB framework, through
qb-io, tells the operating system (via efficient mechanisms like epoll, kqueue, or IOCP, abstracted by the internallibevlibrary) that it's interested in knowing when this operation is ready (e.g., when data arrives). -
Return Immediately: The calling thread (typically a
VirtualCoreexecuting an actor) does not wait. It immediately returns to its event loop, free to process other events for other actors or perform other tasks. - OS Notification: When the I/O operation can proceed without blocking (e.g., data is available for reading, or a socket is ready for writing), the operating system notifies QB's event loop.
-
Dispatch to Handler: The event loop identifies which operation is ready and dispatches this readiness notification. In the context of actors using
qb-iofeatures, this usually means invoking an appropriateon()handler (e.g.,on(MyProtocol::Message&)when data forms a complete message, oron(qb::io::async::event::disconnected&)if a connection drops).
This non-blocking philosophy is key to maximizing CPU utilization and enabling QB applications to handle thousands of concurrent connections or operations with high throughput.
At the core of QB's asynchronous I/O is the event loop, managed by the qb::io::async::listener class (defined in qb/io/async/listener.h).
-
Engine: It wraps the
libevlibrary, a mature and high-performance C event loop. -
Thread-Local Instance: Each
VirtualCore(worker thread managed byqb::Main) runs its own dedicatedlistenerinstance. This instance is accessible within that thread viaqb::io::async::listener::current. -
Monitoring Event Sources: The
listenerdiligently monitors various event sources:-
File Descriptors: For read/write readiness on sockets, files (leading to
qb::io::async::event::io). -
Timers: For scheduled, delayed, or periodic execution of code (leading to
qb::io::async::event::timer). -
System Signals: For handling OS signals like SIGINT, SIGTERM asynchronously (
qb::io::async::event::signal). -
File System Changes: For watching directories or files for modifications (
qb::io::async::event::file).
-
File Descriptors: For read/write readiness on sockets, files (leading to
-
Dispatching: When an event source becomes active, the
listenerdetermines the registered handler (often an I/O component within an actor or a dedicated callback mechanism) and invokes its corresponding method (e.g.,on(qb::io::async::event::timer&)).
(See also: QB-IO: Asynchronous System (qb::io::async))
The event loop must be actively "run" to process pending events.
-
Actors &
qb::Main: If you're usingqb-coreand actors, you generally do not need to callqb::io::async::run()manually. Theqb::Mainengine ensures that eachVirtualCorecontinuously runs its associatedlistenerevent loop as part of its main actor-processing cycle (VirtualCore::__workflow__). -
Standalone
qb-io: If you are using theqb-iolibrary without theqb-coreactor system, your application is responsible for driving the event loop in its main thread or any worker threads that useqb-io's asynchronous features. This is done by callingqb::io::async::run(flag), whereflagcan beEVRUN_NOWAIT(check once and return) orEVRUN_ONCE(wait for and process one block of events), or0(default, runs untilbreak_loop()is called or no active watchers remain).
One of the most direct ways for an actor (or any code running on a VirtualCore thread) to perform actions asynchronously or after a delay is using qb::io::async::callback (from qb/io/async/io.h).
- Purpose: Executes a provided callable (lambda, function pointer, functor) after a specified delay within the event loop of the calling thread.
-
Usage Example (within an Actor):
#include <qb/actor.h> // For qb::Actor, qb::ActorId #include <qb/io/async.h> // For qb::io::async::callback #include <qb/io.h> // For qb::io::cout #include <chrono> // For std::chrono::seconds struct MyDelayedTaskEvent : qb::Event {}; class MyActor : public qb::Actor { public: bool onInit() override { registerEvent<MyDelayedTaskEvent>(*this); qb::io::cout() << "Actor [" << id() << "]: Scheduling immediate task.\n"; // Schedule a lambda to execute in the next event loop iteration on this core qb::io::async::callback([self_id = id()]() { // This lambda runs on the same VirtualCore as MyActor // Best practice: send an event back to the actor to handle the logic if (VirtualCore::_handler) { // Ensure VirtualCore handler is accessible VirtualCore::_handler->push<MyDelayedTaskEvent>(self_id, self_id); } }); qb::io::cout() << "Actor [" << id() << "]: Scheduling task in 2.5 seconds.\n"; double delay_seconds = 2.5; qb::io::async::callback([self_id = id()]() { qb::io::cout() << "Actor [" << self_id << "]: Delayed task (2.5s) executing!\n"; VirtualCore::_handler->push<MyDelayedTaskEvent>(self_id, self_id); }, delay_seconds); return true; } void on(const MyDelayedTaskEvent& /*event*/) { if (!is_alive()) return; // Good practice if callback might outlive actor qb::io::cout() << "Actor [" << id() << "]: Processed a MyDelayedTaskEvent.\n"; } // ... other handlers, including qb::KillEvent ... };
-
Self-Managing Lifetime: The underlying mechanism for
qb::io::async::callback(an internalqb::io::async::Timeoutobject) manages its own lifetime, registering with the listener and automatically cleaning up after the callback executes. -
Actor Safety: When capturing
thisorid()in a callback lambda, always checkis_alive()at the beginning of the lambda if there's a chance the actor might be terminated before the callback runs. -
Common Use Cases for Actors:
- Implementing timeouts for operations.
- Scheduling retries for failed operations (e.g., network connection attempts).
- Breaking down long-running, non-blocking tasks into smaller chunks to yield control back to the event loop periodically.
- Simulating processing delays in examples or tests.
(Reference: example1_async_io.cpp, test-actor-delayed-events.cpp, file_processor/file_worker.h for wrapping blocking calls**)**
For classes (including actors, though async::callback is often more idiomatic for actor-specific timeouts) that need more explicit timeout management or recurring timer events, the qb::io::async::with_timeout<Derived> CRTP base class (qb/io/async/io.h) is available.
-
Inheritance: Your class inherits from
with_timeout<MyClass>. -
Handler: Implement
void on(qb::io::async::event::timer const&)to be called when the timer expires. -
Control:
- The constructor
with_timeout(timeout_seconds)sets and starts the initial timer. -
updateTimeout(): Call this upon relevant activity to reset the timeout countdown. -
setTimeout(new_duration_seconds): Changes the timeout interval. Use0.0to disable.
- The constructor
// Example: An actor session that times out due to inactivity
class InactiveSession : public qb::Actor,
public qb::io::async::with_timeout<InactiveSession> {
public:
InactiveSession() : with_timeout(60.0) { /* 60-second inactivity timeout */ }
bool onInit() override {
registerEvent<ClientActivityEvent>(*this);
registerEvent<qb::KillEvent>(*this);
updateTimeout(); // Start the initial countdown
return true;
}
void on(const ClientActivityEvent& /*event*/) {
qb::io::cout() << "Session [" << id() << "]: Activity detected, resetting timeout.\n";
updateTimeout(); // Reset the timer on client activity
}
// Called by with_timeout if updateTimeout() isn't called within 60s
void on(qb::io::async::event::timer const& /*event*/) {
qb::io::cout() << "Session [" << id() << "]: Inactivity timeout! Terminating.\n";
kill();
}
void on(const qb::KillEvent& /*event*/) { setTimeout(0.0); kill(); }
};(Reference: test-async-io.cpp::TimerHandler, chat_tcp/server/ChatSession.h uses this for client inactivity.)**
When actors use qb-io components for networking or file watching (often via qb::io::use<> helper templates), they receive notifications about I/O status changes through specific event types derived from qb::io::async::event::base.
Common events handled by I/O-enabled actors include:
-
qb::io::async::event::disconnected: Signals that a network connection has been closed or lost. -
qb::io::async::event::eof(End-Of-File): Indicates no more data is available for reading from an input stream. -
qb::io::async::event::eos(End-Of-Stream): Signals that all buffered output data has been successfully written to the transport. -
qb::io::async::event::file: Used byfile_watcherordirectory_watcherto notify about changes to a monitored file or directory.
These events are delivered to the actor's corresponding on(EventType&) handlers, allowing the actor to react to I/O state changes asynchronously.
(Reference: The qb/io/async/event/all.h header includes all standard I/O event types. See specific examples like chat_tcp for their usage.**)
By leveraging this asynchronous I/O model, QB actors can efficiently manage numerous concurrent operations, making the framework well-suited for building high-performance, responsive applications.
(Next: Core Concepts: Concurrency and Parallelism in QB) (See also: Core & IO Integration Overview)**