-
-
Notifications
You must be signed in to change notification settings - Fork 11
concurrency.md
The QB Actor Framework is designed to make concurrent programming more manageable and to effectively utilize modern multi-core processors for parallel execution. It achieves this through a clear separation of concerns managed by the qb::Main
engine and its VirtualCore
worker threads.
It's important to distinguish these two terms in the context of QB:
-
Concurrency: This refers to the ability of the system to handle multiple tasks or actors seemingly at the same time. Even on a single CPU core, actors can make progress independently, especially when some are waiting for I/O (handled asynchronously by
qb-io
) while others are processing events. QB provides concurrency by allowing many actors to exist and react to messages without blocking each other unnecessarily. -
Parallelism: This refers to the ability of the system to execute multiple tasks or actors literally at the same time, by running them on different physical CPU cores. QB achieves parallelism by distributing actors across multiple
VirtualCore
threads, each of which can be mapped to a physical core.
(qb/core/Main.h
)
The qb::Main
class is the central controller of your actor system. Its primary responsibilities include:
-
VirtualCore Management: It creates and manages a pool of
VirtualCore
instances. By default, it often attempts to match the number of available hardware cores, but this is configurable. -
Actor Distribution: When you add actors to the system using
main.addActor<MyActor>(core_id, constructor_args...)
, you specify whichVirtualCore
(by itscore_id
) will be responsible for that actor. -
System Lifecycle:
qb::Main
controls the startup (start()
) and shutdown (stop()
,join()
) of the entire actor system, including allVirtualCore
threads. -
Error Reporting: It can detect if a
VirtualCore
terminates unexpectedly (e.g., due to an unhandled exception in an actor) viamain.hasError()
.
(qb/core/VirtualCore.h
)
A VirtualCore
is essentially a dedicated worker thread that hosts and executes a subset of the system's actors.
-
Event Loop Hub: Each
VirtualCore
runs its own independentqb::io::async::listener
event loop. This loop is the engine that drives all activity on that core, processing I/O events, timer events, and actor messages. -
Actor Mailboxes & Event Queues:
-
Inter-Core Mailbox: Each
VirtualCore
has an incoming MPSC (Multiple-Producer, Single-Consumer) queue. When an actor on another core sends a message to an actor on this core, the message (or its data) is placed into this mailbox. -
Local Event Queue: Events sent between actors residing on the same
VirtualCore
(e.g., viapush()
) are typically handled through a more direct local queueing mechanism within that core.
-
Inter-Core Mailbox: Each
-
Sequential Actor Execution (Crucial Guarantee): Within a single
VirtualCore
, actors execute their event handlers (on(Event&)
methods) and callbacks (onCallback()
) one at a time, to completion. TheVirtualCore
processes one event for one actor fully before picking up the next event for any actor on that core. This fundamental guarantee eliminates data races on an actor's internal state from its own event handlers, simplifying state management significantly. -
Scheduling Logic (Simplified): In each iteration of its main loop, a
VirtualCore
typically:- Checks for and processes I/O events from its
listener
(socket readiness, file system changes, timer expirations fromqb::io::async::callback
orqb::io::async::with_timeout
). - Dequeues and processes events from its inter-core mailbox.
- Dequeues and processes events from its local event queue.
- Invokes
onCallback()
for all actors on this core that have registered viaqb::ICallback
. - Flushes any outgoing event pipes to other cores.
- If configured with a non-zero latency and currently idle, it may briefly sleep.
- Checks for and processes I/O events from its
(qb/core/Main.h
- SharedCoreCommunication
, qb/system/lockfree/mpsc.h
)
When an actor on CoreA
sends an event to an actor on CoreB
, QB handles the communication efficiently and (mostly) transparently:
-
Serialization: The sending
VirtualCore
(CoreA) prepares the event data for transfer. For complex events, this might involve serialization; for simple events or those usingstd::shared_ptr
for payloads, it primarily involves packaging pointers or small data. -
Enqueue to Mailbox: CoreA places the event (or a reference/pointer to its data) into the dedicated MPSC mailbox of the destination
VirtualCore
(CoreB). - Notification (Potentially): CoreA might notify CoreB that new data is available in its mailbox, especially if CoreB was sleeping due_to_idle_latency.
- Dequeue & Dispatch: CoreB, during its event loop cycle, dequeues the event data from its mailbox.
-
Delivery: CoreB reconstructs the event if necessary and dispatches it to the target actor's appropriate
on()
handler.
-
Underlying Mechanism: This inter-core communication relies on high-performance, lock-free MPSC queues (
qb::lockfree::mpsc::ringbuffer
). This design minimizes contention between producing cores and ensures efficient delivery to the single consuming core. -
Ordering Guarantees:
-
push<Event>(dest, ...)
: Events sent from a specific actor A to a specific actor B will be processed by B in the order A pushed them, even if A and B are on different cores. -
send<Event>(dest, ...)
: Provides no ordering guarantees, even for same-core communication.
-
(Reference Example: test-actor-event.cpp
(Multi core tests), test-actor-service-event.cpp
illustrate inter-core messaging.**)
While QB handles much of the complexity, you can fine-tune how VirtualCore
threads behave before starting the engine (main.start()
):
-
CPU Affinity (
main.core(core_id).setAffinity(CoreIdSet)
): Pin aVirtualCore
thread to one or more specific physical CPU cores. This can be beneficial for cache performance and reducing thread migration overhead for critical actors. It requires careful consideration to avoid overloading physical cores. -
Event Loop Latency (
main.core(core_id).setLatency(nanoseconds)
): Control how long aVirtualCore
might sleep if it finds no immediate work.-
0
(default): Lowest latency, but the core spins at 100% CPU even if idle. -
>0
: Allows the core to sleep, reducing CPU usage at the cost of potentially introducing a slight delay in picking up new events. A small value (e.g., a few hundred microseconds to a millisecond) often provides a good balance.
-
Thoughtful configuration of actor placement, core affinity, and latency allows you to tailor QB's parallelism to your application's specific workload and hardware.
(Reference Guide: Guides: Performance Tuning Guide) (Next: QB-IO Module Overview or QB-Core Module Overview)**