Concurrency and Real-Time ProgrammingIntroductionModern mechatronic systems rarely perform one activity at a time. A robot controller may read sensors, estimate position, control actuators, communicate with a supervisory computer, record data, and respond to safety events concurrently. Object-oriented C++ provides several mechanisms for expressing such behavior, including threads, tasks, mutexes, condition variables, atomic objects, futures, promises, and event-driven architectures. Concurrency means that multiple activities are in progress during overlapping time intervals. Parallelism is the simultaneous execution of activities on different processing resources. A concurrent program may run on one processor through interleaving, whereas a parallel program requires simultaneous execution. This distinction is important: concurrency is primarily a software-structure concept; parallelism is an execution-resource concept. Real-time programming adds a timing requirement. A real-time system must respond within specified deadlines. The result is not sufficient by itself; the time at which the result is produced also matters. A hard real-time deadline must never be missed, while a soft real-time deadline may occasionally be missed with an acceptable degradation of service. This chapter develops safe and analyzable approaches to concurrent C++ programming, with particular attention to robotics, embedded control, industrial automation, and data acquisition. Learning ObjectivesBy the end of this chapter, students should be able to: - Distinguish concurrency, parallelism, multitasking, and real-time execution.
- Create and manage C++ threads and tasks safely.
- Explain data races, race conditions, deadlock, starvation, livelock, and priority inversion.
- Select suitable mutexes, locks, condition variables, atomics, futures, and event queues.
- Design producer–consumer and event-driven systems.
- Analyze latency, jitter, determinism, and deadline feasibility.
- Apply best practices for exception safety, cancellation, shutdown, and testing.
1. A Model of Concurrent ExecutionA sequential program has one principal flow of control. A concurrent program has several flows whose operations may be interleaved. If two threads access the same memory location, the correctness of the program depends on the ordering and synchronization of those accesses. A thread is an independently schedulable execution path within a process. Threads in the same process share memory and resources, which makes communication efficient but also creates synchronization risks. A task is a unit of work whose result can often be represented by a future. Tasks may be implemented by threads, a thread pool, or an operating-system scheduler. The C++ memory model defines relationships between evaluations. A data race occurs when two potentially concurrent evaluations access the same memory location, at least one access is a write, and the accesses are not properly ordered by synchronization. A data race produces undefined behavior. This is stronger than merely obtaining an incorrect value: the compiler is allowed to make transformations that assume data races do not occur. A race condition is a broader logical defect in which correctness depends on timing or ordering. Some race conditions involve a formal data race; others arise when individually synchronized operations do not preserve a required multi-step invariant. 1.1 Processes, threads, and shared stateProcesses normally have separate address spaces. Threads within one process share objects such as global variables, dynamically allocated memory, and class instances. Shared state should therefore be minimized. A useful design principle is: Prefer communicating immutable values or ownership transfer over unrestricted sharing of mutable objects. Object-oriented design remains valuable in concurrent systems. A class can encapsulate its state and synchronization policy, expose thread-safe operations, and prevent clients from directly manipulating protected data. However, a method being individually thread-safe does not automatically make a sequence of method calls atomic as a transaction. 2. Creating Threads in C++The header <thread> provides std::thread. A thread begins execution when constructed with a callable object and continues until the callable returns or throws. If an exception escapes the thread function, the program calls std::terminate; therefore exceptions should be handled inside the thread or transferred explicitly. #include <iostream>
#include <thread>
void acquireSensorData() {
std::cout << "Acquiring data\n";
}
int main() {
std::thread sensorThread(acquireSensorData);
sensorThread.join();
}join() waits for completion and makes the thread’s resources available for reclamation. A joinable thread must be joined or detached before its destructor runs; otherwise the destructor calls std::terminate. detach() allows the thread to continue independently, but it makes lifetime management difficult. Detached threads should be avoided unless their lifetime and access to objects are rigorously controlled.
C++20 also provides std::jthread, which automatically joins during destruction and supports cooperative cancellation through std::stop_token. For modern code, std::jthread is often safer than manually managing a std::thread. #include <chrono>
#include <thread>
void worker(std::stop_token stop) {
while (!stop.stop_requested()) {
// Perform one bounded unit of work.
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::jthread t(worker);
// Destruction requests stopping and joins the thread.
}A stop request is cooperative. It does not forcibly interrupt a thread. The worker must check the token or use cancellation-aware waiting. 3. Mutual Exclusion and LockingA mutex protects a critical section: a region in which shared state is accessed under exclusive ownership. The simplest form is std::mutex and std::lock_guard. #include <mutex>
class Counter {
private:
int value_{0};
mutable std::mutex mutex_;
public:
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++value_;
}
int value() const {
std::lock_guard<std::mutex> lock(mutex_);
return value_;
}
};RAII is essential: the lock is released automatically when the guard leaves scope, including during exception propagation. std::unique_lock provides deferred locking, manual unlocking, and compatibility with condition variables. std::scoped_lock can lock multiple mutexes using a deadlock-avoidance algorithm. std::scoped_lock lock(firstMutex, secondMutex); A recursive mutex permits the same thread to lock a mutex more than once. It can be useful in limited designs, but it may conceal poor ownership structure and should not be the default solution. 3.1 Lock granularityA coarse-grained lock protects a large object or operation. It is simple but may reduce parallelism. A fine-grained design uses several locks, improving concurrency but increasing the possibility of deadlock and inconsistent snapshots. The protected invariant should determine the lock boundary, not an attempt to maximize the number of locks. A lock should normally be held for the shortest interval that preserves correctness. It should not be held while performing blocking I/O, calling unknown external code, or executing lengthy computation unless that behavior is explicitly required. 3.2 Deadlock and lock orderingDeadlock is a state in which progress is impossible because each participant waits for a resource held by another. The classical necessary conditions are mutual exclusion, hold-and-wait, no pre-emption, and circular wait. Preventing at least one condition prevents deadlock. A practical prevention method is global lock ordering. Assign an order to mutexes and always acquire them in increasing order. std::scoped_lock is preferable when multiple mutexes must be acquired together. Other liveness problems include: - Starvation: a thread waits indefinitely because other threads repeatedly receive service.
- Livelock: threads remain active but repeatedly respond to one another without making progress.
- Priority inversion: a high-priority task waits for a lock held by a lower-priority task, possibly while medium-priority tasks execute.
Real-time operating systems may support priority inheritance or priority-ceiling protocols. These mechanisms reduce, but do not eliminate, the need for careful design. 4. Condition Variables and Producer–Consumer DesignsA condition variable allows a thread to sleep until a state predicate may have become true. It is commonly used with a mutex and a queue. #include <condition_variable>
#include <mutex>
#include <queue>
class CommandQueue {
std::queue<int> queue_;
std::mutex mutex_;
std::condition_variable condition_;
bool stopped_{false};
public:
void push(int command) {
{
std::lock_guard<std::mutex> lock(mutex_);
if (stopped_) return;
queue_.push(command);
}
condition_.notify_one();
}
bool pop(int& command) {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] {
return stopped_ || !queue_.empty();
});
if (queue_.empty()) return false;
command = queue_.front();
queue_.pop();
return true;
}
void stop() {
{
std::lock_guard<std::mutex> lock(mutex_);
stopped_ = true;
}
condition_.notify_all();
}
};The predicate form of wait is important because condition variables may experience spurious wakeups. A notification is not a guarantee that the desired condition is true. The waiting thread must re-check the predicate while holding the mutex. The queue’s shutdown state is part of its protocol. Without notification during shutdown, a consumer could remain blocked indefinitely. Bounded queues are often preferable in control systems because they make memory use predictable. When a bounded queue is full, the design must specify whether new data is rejected, old data is discarded, the producer blocks, or an overflow fault is raised. 5. Atomic Objects and the Memory Modelstd::atomic<T> provides indivisible operations for suitable types and establishes ordering relationships between threads. It is appropriate for flags, counters, state indicators, and some lock-free data structures. It is not a universal replacement for a mutex.
#include <atomic>
std::atomic<bool> emergencyStop{false};
void monitor() {
if (emergencyStop.load(std::memory_order_acquire)) {
// Enter a safe state.
}
}
void tripStop() {
emergencyStop.store(true, std::memory_order_release);
}A release store and an acquire load can establish a synchronizes-with relationship when the load observes the stored value. This can make preceding writes visible to the acquiring thread. memory_order_seq_cst, the default, provides the strongest and simplest ordering model, though potentially with greater cost. Relaxed ordering guarantees atomicity but does not, by itself, establish ordering of unrelated memory operations. Atomic operations are suitable when the shared state is simple and the required ordering is clearly understood. A mutex is usually more appropriate when several variables form one invariant. For example, an atomic queue size and a non-atomic queue container do not automatically create a safe queue. The term lock-free is frequently misunderstood. A lock-free implementation guarantees that system-wide progress occurs, not that every individual thread completes within a bounded time. Lock-free does not necessarily mean wait-free, and neither property alone guarantees real-time suitability. 6. Tasks, Futures, Promises, and Asynchronous Operationsstd::async launches a callable and returns a std::future representing its result. The future can transfer a value or an exception from the worker to the caller.
#include <future>
std::future<double> estimatePosition() {
return std::async(std::launch::async, [] {
return 1.25;
});
}
int main() {
auto result = estimatePosition();
double position = result.get();
}The launch policy matters. std::launch::async requests asynchronous execution, whereas std::launch::deferred postpones execution until get() or wait(). The default policy may choose either. Explicit policies are preferable when timing or execution behavior matters. A future can be waited upon, queried, or used to retrieve a result. Calling get() more than once is invalid. If the asynchronous function throws, get() rethrows the exception in the waiting thread. std::promise<T> is the producer side of a future. It is useful when a result is generated by a manually managed thread or callback. For repeated events, a queue or event stream is generally more suitable than repeatedly creating futures.
In latency-sensitive control loops, dynamically creating tasks may introduce unpredictable allocation, scheduling, and synchronization overhead. A preallocated worker pool or fixed periodic thread is often more analyzable. 7. Event-Driven and Actor-Style DesignIn an event-driven design, components react to messages such as sensor updates, timer events, commands, and faults. Instead of exposing mutable state, an object processes events through a controlled execution context. This reduces shared-memory access and can make behavior easier to test. A typical architecture contains: - Device interfaces that acquire raw data.
- A bounded event queue.
- A dispatcher or executor.
- Domain objects that process events.
- Output and safety components.
Events should be immutable value objects where practical. Each event can contain a type, timestamp, sequence number, and payload. Timestamps should use a monotonic clock for interval measurement; wall-clock time can be adjusted and is unsuitable for measuring elapsed durations. Event-driven systems still require policies for queue capacity, event priority, cancellation, ordering, and overload. A queue that silently grows without limit may convert a temporary overload into an eventual memory failure and unbounded latency. 8. Real-Time Concepts and Timing AnalysisA real-time task is commonly characterized by a release time, execution time, period, deadline, and priority. Let: - Ci be the worst-case execution time of task i, measured in seconds;
- Ti be its period or minimum inter-arrival time, in seconds;
- Di be its relative deadline, in seconds.
For a periodic task set on one processor, utilization is: U = ∑(Ci / Ti) Here U is dimensionless. A necessary condition for schedulability is U ≤ 1 on a single processor, because total required execution time cannot exceed available processor time. This condition is not sufficient for every scheduling policy, especially when deadlines differ from periods, blocking occurs, or execution times vary. For independent periodic tasks scheduled by earliest-deadline-first on an ideal single processor, utilization no greater than 1 is sufficient under standard assumptions, including pre-emption and suitable deadline and release models. Fixed-priority rate-monotonic scheduling has stronger analytical bounds and assumptions; practical systems require response-time analysis that includes blocking, interference, release jitter, and operating-system overhead. Latency is the elapsed time between an event and the corresponding response. Jitter is variation in latency or in periodic release times. A system can have low average latency but unacceptable worst-case latency. Real-time engineering therefore emphasizes bounded worst-case behavior rather than averages alone. A simple response-time model for a task includes computation, higher-priority interference, blocking, and release effects: Ri = Ci + Bi + ∑j∈hp(i) ⌈Ri / Tj⌉ Cj In this iterative equation, Ri is the worst-case response time of task i, Bi is its maximum blocking time, and hp(i) is the set of higher-priority tasks. The ceiling term counts possible interfering releases. The equation applies only under a specified fixed-priority model and requires appropriate assumptions about pre-emption, independent execution, and bounded blocking. A task meets its deadline when the converged response time satisfies Ri ≤ Di. Worked example: utilization screeningSuppose three periodic tasks have the following parameters: | Task | Ci | Ti |
|---|
| Sensor acquisition | 1 ms | 10 ms | | Control calculation | 2 ms | 20 ms | | Diagnostics | 3 ms | 100 ms |
Their utilization is: U = 1/10 + 2/20 + 3/100 = 0.10 + 0.10 + 0.03 = 0.23 Thus 23% of one processor is required under the stated execution-time assumptions. This is only a screening result. Interrupts, context switches, blocking, cache effects, communication, and execution-time variation must still be considered before claiming deadline satisfaction. 9. Designing a Periodic Control LoopA control loop should use a monotonic clock and a schedule based on absolute release times. Sleeping for a fixed duration after computation accumulates computation time and causes drift. The following pattern reduces drift: using clock = std::chrono::steady_clock;
auto next = clock::now();
const auto period = std::chrono::milliseconds(10);
while (!stopRequested()) {
next += period;
readSensors();
calculateControl();
writeActuators();
std::this_thread::sleep_until(next);
}If computation exceeds the period, the loop is late. The system must define a policy: skip obsolete work, execute immediately to catch up, reduce workload, enter a degraded mode, or trigger a safety response. Blindly executing every missed iteration may increase backlog and worsen latency. A hard real-time control path should avoid unbounded operations such as uncontrolled dynamic allocation, blocking locks, file I/O, network calls, and console output. These operations may be acceptable in a supervisory or logging thread but should be separated from the safety-critical path. 10. Illustrative Case: Autonomous Conveyor CellBackgroundConsider an illustrative industrial conveyor cell. A C++ controller reads a photoelectric sensor, estimates the position of packages, commands a servo motor, records production data, and stops the cell when a guard switch opens. ProblemAn initial implementation uses one shared package vector. The sensor thread appends measurements while the control thread iterates through the vector. A logging thread periodically copies the vector. Operators observe occasional crashes, irregular motor response, and delayed emergency stopping. AnalysisThe vector accesses create a data race. Adding one mutex around individual push_back and element reads would prevent memory corruption but might still produce an inconsistent multi-step snapshot. The emergency signal is also checked only after a long logging operation, producing unacceptable response latency. Finally, the logging thread can hold a mutex while writing to disk, blocking control calculations. Approach and solutionThe redesigned architecture uses: - An atomic emergency-stop flag for the immediate stop request.
- A bounded sensor-event queue protected by a mutex and condition variable.
- A control thread that owns the active package model.
- Immutable snapshots copied to a logging queue.
- A separate logging thread for disk operations.
- A monotonic periodic schedule for the control loop.
- A shutdown protocol that stops producers, wakes consumers, drains or rejects queued work according to policy, and joins all threads.
The control thread does not share its mutable package model with the logger. It publishes a snapshot as a value. The emergency path is designed so that the actuator interface can enter a safe state without waiting for the logging mutex. The system records deadline misses and queue overflows as diagnosable faults. OutcomeThe design removes the vector data race, prevents disk I/O from blocking control computation, and gives the emergency path a bounded software response subject to the actuator and operating-system specifications. Testing under overload reveals queue capacity and deadline behavior rather than allowing failures to remain intermittent. Learning pointsThe case demonstrates that synchronization is an architectural concern. A mutex can protect memory, but ownership separation, bounded queues, priority policy, and timing analysis are needed to achieve predictable behavior. 11. Testing and VerificationConcurrent programs require more than ordinary functional tests. Useful techniques include: - Stress tests with varied thread interleavings.
- Thread sanitizers for detecting many data races and lock errors.
- Static analysis and code review for lock ordering.
- Fault injection involving delayed consumers, queue overflow, and cancellation.
- Measurement of minimum, maximum, average, and percentile latency.
- Deadline-miss counters and execution-time instrumentation.
- Model-based tests for event sequences and shutdown protocols.
Timing measurements must identify the hardware, compiler, operating-system configuration, optimization level, workload, and measurement overhead. An observed maximum is not automatically a mathematical worst-case bound. Certification-oriented systems require documented requirements, traceability, verification evidence, and controlled configuration. 12. Safety, Ethics, and Professional PracticeIn mechatronics, concurrency defects can cause physical harm. Safety functions should be designed according to the applicable machinery, functional-safety, and electrical-safety standards for the jurisdiction and application. Software should not assume that an ordinary thread-level flag alone satisfies a certified emergency-stop requirement; hardware safety circuits, independent monitoring, and validated actuator behavior may be necessary. Engineers should make timing assumptions explicit, document ownership and lock policies, review failure modes, and avoid claiming real-time guarantees without evidence. Logging must also respect privacy and cybersecurity requirements when sensors capture people, production information, or network data. 13. Contemporary C++ PracticeModern C++ favors RAII, std::jthread, scoped locks, explicit ownership, immutable messages, and standard synchronization facilities. C++20 introduced additional tools such as atomic waiting and semaphores; C++20 and later standards also support richer synchronization patterns. These facilities improve clarity but do not automatically provide deterministic scheduling. Executors and senders/receivers are important directions for structured asynchronous programming, although availability and interfaces depend on the selected C++ standard library and implementation. In embedded systems, a real-time operating system may provide fixed-priority scheduling, bounded synchronization primitives, memory pools, and timing services that are more appropriate than general-purpose desktop facilities. Best-Practice Checklist- Define ownership of every mutable object.
- Minimize shared mutable state.
- Use RAII-based locks.
- Establish and document lock ordering.
- Wait on condition-variable predicates.
- Design explicit startup, cancellation, and shutdown protocols.
- Use atomics only when the required memory ordering is understood.
- Keep hard real-time paths bounded and free from unnecessary allocation and blocking.
- Prefer monotonic clocks for elapsed-time measurement.
- Measure worst-case behavior and test overload, faults, and deadline misses.
- Separate control, communication, logging, and diagnostics when their timing needs differ.
- Treat safety requirements as system requirements, not merely programming details.
Chapter SummaryConcurrency allows a C++ system to manage multiple activities whose execution overlaps; parallelism additionally requires simultaneous processing. Threads provide execution paths, while tasks and futures provide structured result handling. Shared mutable state must be protected against data races using suitable synchronization, including mutexes, condition variables, or atomic operations. RAII locks and consistent lock ordering reduce exception and deadlock risks. Condition variables support efficient producer–consumer communication when paired with a correctly maintained predicate. Atomics provide indivisible operations and memory-ordering relationships but are not a general replacement for mutexes. Event-driven architectures and immutable messages reduce shared-state complexity. Real-time systems must be evaluated by latency, jitter, response time, utilization, and deadline satisfaction rather than average throughput alone. Safe designs include bounded queues, explicit shutdown, priority-aware policies, measured execution times, and separation of time-critical work from blocking I/O. Key TermsThread: An independently schedulable execution path within a process. Concurrency: Progress of multiple activities during overlapping time intervals. Parallelism: Simultaneous execution on multiple processing resources. Task: A unit of work, often associated with a result or future. Data race: Unsynchronized conflicting accesses to the same memory location, producing undefined behavior. Race condition: A defect in which correctness depends on timing or ordering. Mutex: An exclusive synchronization object protecting a critical section. RAII: Resource management in which object lifetime controls acquisition and release. Deadlock: Permanent mutual waiting among tasks or threads. Starvation: Indefinite failure of a task to obtain needed service. Livelock: Repeated activity without useful progress. Priority inversion: Blocking of a high-priority task by lower-priority work. Condition variable: A mechanism for sleeping until a shared-state predicate may change. Atomic operation: An indivisible operation on a shared object. Memory ordering: Rules governing visibility and ordering of operations between threads. Future: An object representing a value or exception that may become available later. Latency: Time from an event or request to its corresponding response. Jitter: Variation in timing, such as release or response time. Hard real-time: A system in which missing a deadline is unacceptable. Worst-case execution time: An upper execution-time estimate or bound used for timing analysis. Review QuestionsShort Answer Questions- Distinguish concurrency from parallelism.
- Why is a condition-variable predicate necessary?
- What problem does RAII solve when used with mutexes?
- Explain why an atomic flag is not necessarily sufficient to protect a multi-variable invariant.
- Why should a monotonic clock be used to measure elapsed time?
Long Answer Questions- Explain the causes of deadlock and compare lock ordering with
std::scoped_lock as prevention techniques. - Describe the design of a bounded producer–consumer queue, including shutdown behavior.
- Compare mutexes, atomics, futures, and event queues for communication between C++ components.
- Explain latency, jitter, utilization, response time, and deadline feasibility in a real-time controller.
- Discuss how object-oriented encapsulation can reduce concurrency defects without eliminating the need for system-level analysis.
Analytical / Application Questions- A controller has tasks with execution times of 1 ms, 2 ms, and 3 ms and periods of 10 ms, 20 ms, and 100 ms. Calculate utilization and explain why the result alone cannot prove deadline satisfaction.
- Design a shutdown protocol for a sensor thread, control thread, event queue, and logging thread. Identify possible blocking and data-loss decisions.
- Analyze the conveyor-cell case. Identify the data race, the source of latency, and an architecture that separates safety-critical control from logging.
Multiple Choice Questions with AnswersQuestion 1Which statement best distinguishes concurrency from parallelism? A. Concurrency requires multiple processors, while parallelism requires one. B. Concurrency concerns overlapping progress, while parallelism concerns simultaneous execution. C. Concurrency is only used for real-time systems. D. Parallelism prevents all data races automatically. Correct Answer: B. Concurrency concerns overlapping progress, while parallelism concerns simultaneous execution. Question 2What must a thread do before a joinable std::thread is destroyed? A. Call detach only B. Call either join or detach C. Call sleep D. Acquire a mutex Correct Answer: B. Call either join or detach Question 3Why should a condition variable normally be waited on with a predicate? A. To increase processor frequency B. To prevent all context switches C. Because wakeups do not guarantee that the desired condition is true D. Because mutexes cannot be used with condition variables Correct Answer: C. Because wakeups do not guarantee that the desired condition is true Question 4Which construct provides automatic mutex release when the guard leaves scope? A. RAII lock guard B. Raw pointer C. Detached thread D. Relaxed atomic Correct Answer: A. RAII lock guard Question 5Which situation is a data race? A. Two threads read an immutable object B. Two threads access a variable, one writes, without synchronization C. A thread waits on a future D. A task uses a local variable Correct Answer: B. Two threads access a variable, one writes, without synchronization Question 6What is the main purpose of a release store paired with an acquire load? A. To guarantee infinite execution speed B. To establish visibility and ordering between threads when the load observes the store C. To make every operation lock-free D. To eliminate the need for a scheduler Correct Answer: B. To establish visibility and ordering between threads when the load observes the store Question 7For tasks with execution times 1 ms, 2 ms, and 3 ms and periods 10 ms, 20 ms, and 100 ms, respectively, what is processor utilization? A. 0.13 B. 0.23 C. 0.33 D. 2.30 Correct Answer: B. 0.23 Question 8Which operation is generally least suitable inside a hard real-time control path? A. A bounded calculation B. Reading a preallocated data structure C. Unbounded disk I/O D. Checking a cancellation flag Correct Answer: C. Unbounded disk I/O Question 9What does priority inversion describe? A. A high-priority task being delayed by lower-priority work holding a needed resource B. A task changing its result type C. Two processors executing identical instructions D. A queue becoming empty Correct Answer: A. A high-priority task being delayed by lower-priority work holding a needed resource Question 10Why is a bounded event queue useful in a real-time system? A. It guarantees that no event is ever lost B. It makes memory use and overload behavior explicit C. It removes the need for synchronization D. It ensures every task meets its deadline Correct Answer: B. It makes memory use and overload behavior explicit
|