Skip to content

Source Structure and Reading Guide

What this page solves

When behavior may come from the Facade, pool, task graph, or platform layer, do not browse every header. Executor is organized around public contract, resource owner, execution path, and diagnostic path. Find the boundary, then trace one task lifecycle.

This is the current source organization. Facade/types in include/executor/ are integration entry points; schedulers, queues, and synchronization in src/executor/ may be refactored while tests and status APIs preserve externally observable behavior.

Layered architecture

flowchart TD A[Application\nsubmit_auto / future / status / shutdown] --> B[Public Facade\ninclude/executor/executor.hpp] B --> C[Resource owner\nexecutor_manager.cpp] C --> D[ThreadPoolExecutor] C --> E[RealtimeThreadExecutor] C --> F[GPU executor] C --> G[Comm components] D --> H[PriorityScheduler] H --> I[TaskDispatcher] I --> J[WorkerLocalQueue / LockFreeWorkerQueue] J --> K[worker] E --> L[cycle callback + bounded MPSC queue]
LayerPrimary responsibilityMust not own
FacadeConvert calls into observable futures, handles, and failure eventsWorker queues or assumptions about a lock implementation
ManagerOwn executors, singleton/independent instance, registration lifecycleBusiness retry or data idempotency decisions
AdapterMap IAsyncExecutor/IRealtimeExecutor to implementationChange Facade business semantics
Scheduler/queueQueue, dispatch, consume, backpressureCross-task business state
Monitor/diagnosticsStatistics and reportingBlocking in callbacks or changing task result

Read source from the API behavior

BehaviorRead firstThen readVerification
submit_auto(lambda) returns a futureRouting and template APIs in include/executor/executor.hppsrc/executor/thread_pool_executor.cpp, thread_pool.cppFacade/routing, exception, and timeout tests
Dependent task does not runFacade TaskGraphState, submit_after_with_handlesrc/executor/task/task_dependency_manager.cppFacade/dependency tests and tutorial smoke
Priority does not preemptPriorityScheduler::dequeue()TaskDispatcher::dispatch(), worker loopPriority tests and queue status
Resize does not lose tasksThreadPool::resize_local_queues()TaskDispatcher::dispatch_batch() requeue branchResize/concurrent-stop tests
Real-time task dropsExecutor::push_realtime_task()RealtimeThreadExecutor::push_task_ex()Push-overflow tests and status counters
Lock-free queue appears empty/fullsrc/executor/util/lockfree_queue.hppCaller capacity/object-pool logicMPSC benchmark, TSAN/stress

First find object ownership and exit authority: Executor owns a manager in its instance model, manager owns executors, adapters use shared_ptr snapshots for stop/submit races, while the caller owns a realtime cycle_manager and Executor only borrows it.

Synchronization domains are not one global lock

DomainProtected objectTypical primitiveReason
Facade failureCounts, ring buffer, callback snapshotfailure_mutex_Invoke callback after unlock to avoid diagnostic reentry
Facade task graphNode state, dependent maptask_graph_mutex_ + graph shared_mutexState transition/dependency resolution need atomic observation
Manager registryRealtime/GPU registryshared_mutexMany lookups, rare registration; pointer needs lifetime proof
Pool lifecycleStop, totals/completed/activemutex_ + atomicsSeparate submit/stop boundary from completion waiting
Local queuesReplacement worker-queue vectorlocal_queues_mutex_ + atomic shared_ptrSnapshot extends old vector life through resize
LockFreeQueue slotsReady/reuse sequenceacquire/release sequences_Publish data and reuse slots without serializing producers

Atomic does not mean lock-free, and removing one lock does not authorize cross-domain access. Before source changes, draw writers/readers, destruction point, and the condition variable responsible for wakeup.

Two completion invariants

flowchart LR A[accepted task] --> B[scheduler or worker queue] B --> C[active worker] C --> D[completed or failed] E[completion_ready] --> F[scheduler_empty] F --> G[all local queues empty] G --> H[active_threads = 0] H --> I[total_tasks = completed_tasks]

failed_tasks is a subset of completed work, not another term to subtract. If a task dequeued from the scheduler encounters an invalid worker ID or full local queue, dispatcher must requeue it; otherwise a successfully submitted future can never complete.

flowchart LR A[accepted realtime task] --> B[bounded MPSC queue] B --> C[current cycle consumes it] C --> D[wrapper returned to pool] E[rejected task] --> F[dropped_task_count + reason counter]

Stop blocks new producers, waits registered producers out, then drains. No accepted wrapper may appear after final drain.

Validate source changes in order

  1. Write a minimal test that exposes invariant failure: future readiness, count reconciliation, bounded shutdown.
  2. Run targeted tests: graph changes use dependency/Facade tests; queue changes use MPSC/realtime overflow; resize changes use resize/concurrent-stop.
  3. Validate user-visible status APIs and failure events, not only internal variables.
  4. Run TSAN or stress tests; a single local concurrent pass is not race proof.
  5. For performance changes, preserve environment, raw JSON, and correctness reconciliation using performance measurement.

Continue with execution paths, lock-free experiments, or custom cycle sources.