Concurrent algorithm 2026-11-05
A concurrent algorithm coordinates activities whose execution steps can overlap in time or be interleaved. Correctness must cover the allowed thread interleavings, including executions in which one activity pauses while another continues. Merely testing one convenient schedule does not establish the required property.
Suppose two activities increment a shared counter initially zero by separately reading, adding one and writing. Both can read zero before either writes, so the final value is one rather than two. An atomic read-modify-write can make each increment indivisible; mutual exclusion can instead protect a larger critical section. The appropriate choice depends on which shared-state invariant must survive each permitted execution step.
Leslie Lamport's Lamport's bakery algorithm illustrates the distinction between a coordination rule and assumptions about hardware. Its mutual exclusion argument orders ticket/process-number pairs, and the original proof explicitly treats reads overlapping writes rather than silently assuming every shared-memory access behaves atomically.[ref]
A New Solution of Dijkstra's Concurrent Programming Problem by Leslie Lamport. Printed pp.453–454, algorithm and correctness assertions
Concurrent correctness can include safety, such as never entering two critical sections together, and progress, such as a waiting activity eventually proceeding under stated scheduling assumptions. Critical section Created 2026-10-14 Updated 2026-10-25
A region whose accesses must obey a shared synchronization protocol. For the lost update in mutual exclusion, the critical section includes the read and the local addition as well as the write. Choosing its boundary means identifying the complete operation whose intermediate state must be protected. Every conflicting access must participate in a compatible protocol; merely declaring a mutex does not protect accesses that bypass it.
Event counter 2026-10-24
An event counter records a number of observed events, unlike a single-bit event latch which records only whether any event occurred. A software event counter shared with an interrupt handler needs atomic updates or a suitable critical section. Its finite capacity imposes a maximum backlog unless overflow is handled explicitly.
Interrupt-driven pulse sequencer Created 2026-10-25 Updated 2026-10-31
A pulse sequencer can separate event capture from output timing. A short interrupt handler acknowledges the event latch and increments a pending count. An independent finite-state machine consumes one count on entering
ON, changes to GAP after the pulse width, and enters IDLE only after the mandatory off interval. Even an empty queue must not bypass GAP: an arrival immediately after light-off would otherwise start too soon.The pending count equals recorded events minus started pulses. A critical section protects its read-modify-write operations; declaring shared state as in volatile-qualified objects in C does not alone make each update an atomic operation. Separate sequencers for different channels allow simultaneous pulses while one hardware timer schedules their earliest deadline. Finite queue capacity, detector rearming and dispatch resolution remain explicit assumptions.
Interrupt-safe ring-buffer snapshot 2026-10-24
When an interrupt handler is the sole writer of a fixed ring buffer, foreground code can obtain a coherent view by saving and masking the relevant interrupt, copying the count and valid entries, and restoring the earlier mask. Perform expensive calculations on the copy after leaving the critical section. Exclusion prevents the writer from changing the logical version during the copy; individually atomic loads alone do not give this guarantee. Correctness also requires compiler ordering at the boundaries, no unmasked competing writer, and a short enough mask interval that the peripheral does not lose events. Restoring the previous mask avoids accidentally enabling an interrupt disabled by an outer caller.
Lamport's bakery algorithm 2026-11-05
Lamport's bakery algorithm is a mutual exclusion concurrent algorithm in which each participating process chooses a numbered ticket before entering a critical section. Process announces that it is choosing, sets its ticket to one more than the largest observed ticket, and finishes choosing. It then waits for each other process to finish choosing and for every smaller nonzero pair to leave. Comparing identifiers breaks ties between equal tickets; setting one's ticket to zero announces departure.[ref]
For example, simultaneous choices can give processes 2 and 5 the same ticket 7; pair precedes . The choosing flag prevents treating an unfinished choice as an absent contender. Leslie Lamport's 1974 proof even permits an overlapping read to return an arbitrary value under the specified shared-register model. The mathematical model uses unbounded tickets; a bounded machine implementation must also address overflow and its memory model.[ref]
A New Solution of Dijkstra's Concurrent Programming Problem by Leslie Lamport. pp.454–455, register assumptions and correctness
Liveness property 2026-10-14
A requirement of eventual progress, with no finite prefix ruling out its eventual satisfaction. For starvation freedom, a persistent requester must eventually enter its critical section. Establishing such a liveness property requires explicit scheduling and protocol assumptions in addition to mutual exclusion. Exhaustive finite thread interleaving checks alone do not establish eventual service for repeatedly competing software threads.[ref]
Defining Liveness by Bowen Alpern and others. p. 182: every finite prefix can still be extended to satisfy a liveness property
Lost update 2026-10-14
A change that is overwritten by another update based on an older value. In mutual exclusion, both software threads read and later write , so one intended increment is lost. This race condition can occur with individually atomic reads and writes and therefore need not involve a data race. Its repair must cover the complete update, using a critical section or an appropriate atomic read-modify-write.
Mutex 2026-10-14
An object with acquire and release operations that provides exclusive ownership to a participating software thread. Several source-code regions can acquire the same mutex; two different mutex objects do not exclude each other's users. The caller should enter its critical section only after successful acquisition and release ownership on every exit path. A mutex may make a waiting software thread sleep or use a spinlock; the interface and the waiting mechanism are distinct.[ref]
Mutual exclusion Created 2026-10-14 Updated 2026-10-25
Mutual exclusion allows at most one participating software thread at a time to execute a protected critical section. A mutex is an object used to enforce that rule; the critical section is the region of the program that needs the rule. Several different critical sections can use the same mutex to protect the same shared data. The benefit is reasoning about a complete update without another cooperating software thread observing or changing its intermediate state.[ref]
For a small lost update, shared Assume sequential consistency: each step is indivisible, each software thread keeps the displayed order, and a read sees the latest preceding write to
x initially equals . Each of two software threads, T1 and T2, uses its own local r and performs these three steps:r = read(x)
r = r + 1
write(x, r)x. In the thread interleaving T1 read, T2 read, T1 add, T2 add, T1 write, T2 write, both local values become and both writes store . Final instead of : a lost update despite two correctly executed local additions. Separate indivisible reads and writes are insufficient; the complete read, modify and write must be protected.The binomial coefficient counts the thread interleavings preserving the two three-step orders: choose T1's three positions, with the remaining positions belonging to T2. Exhaustive enumeration gives final in 18 orders and in the two serial orders. These counts are not probabilities: no random scheduler has been specified. With the same mutex acquired before reading and released after writing, only the two complete increment orders remain, and both give . Alternatively, an atomic read-modify-write increment updates
x in one indivisible step. Protecting only the write still allows both software threads to read beforehand.Mutual exclusion is a safety property: two participants must never be inside the protected critical section together. Eventual entry for every persistent requester is a liveness property, expressed here as starvation freedom. A protocol that never admits anyone satisfies the first requirement while failing the second. Nor does a mutex alone promise which waiting software thread enters next, or that a producer runs before a consumer. Such application ordering needs an additional synchronization protocol.[ref]
Defining Liveness by Bowen Alpern and others. pp. 181–182: mutual exclusion, finite witnesses to safety violations, and eventual service
The sequential consistency assumptions delimit this calculation. In C++, unsynchronized conflicting accesses to an ordinary shared object can be a data race, which makes behavior undefined; the 20 schedules are not a prediction for that program. An atomic operation with relaxed ordering also supplies no acquire-release synchronization for other data. A real mutex must supply both exclusive entry and the required happens-before ordering between its users.[ref]
A short interrupt handler should acknowledge the detector and count work; a timed finite-state machine should generate the flashes. A Boolean “someone arrived” variable cannot preserve several queued detections. This is an interrupt-driven pulse sequencer. Initialise the light off, the pending-event counter to zero and the sequencer to
IDLE before enabling interrupts.In the following pseudocode, interrupt acknowledgement uses the source-specified operation of writing zero. The atomic operation and timer calls name abstract services, not undocumented device registers of a particular microcontroller.
eye_handler():
if EYE_DETECTED != 0:
EYE_DETECTED = 0
pending = pending + 1
try_take_event():
briefly mask the detector interrupt
if pending == 0: taken = false
else: pending = pending - 1; taken = true
restore the previous interrupt mask
return taken
service(now):
if state == ON and elapsed(now, started) >= one_second:
LIGHT = 0
state = GAP
started = now
if state == GAP and elapsed(now, started) >= one_second:
state = IDLE
if state == IDLE and try_take_event():
LIGHT = 1
state = ON
started = nowThe main program repeatedly handles a new event or the next timer deadline and calls
service. The ON state lasts one second; every completed flash enters GAP, even if the queue is empty at that instant. An arrival during that gap must wait for it to expire. Otherwise turning the light off and immediately treating the device as idle could allow a new flash too soon. Once a gap has expired, a queued event starts a new flash; if no work is pending the device remains idle. Removing a pending event only when a flash starts gives one completed flash per consumed event.An interrupt can arrive during the main program's decrement. Without a critical section, its increment can be overwritten by a race condition. Merely declaring the counter
volatile, as in volatile-qualified objects in C, does not make the decrement an atomic operation. Short interrupt masking, or an appropriate atomic operation, protects it; interrupts must remain enabled during the one-second intervals so that detections continue to be recorded.A calibrated busy-wait loop is simple, but occupies the CPU and its duration depends on instruction timing, compiler transformations and time spent in interrupts. Polling a hardware timer avoids calibration dependence but still consumes CPU while polling. A timer interrupt or timer comparison deadline permits useful work or safe sleeping between transitions and is preferable for precise timing. If sleeping, the test for queued work and entry into sleep must avoid a lost wake-up. By the modulo-counter elapsed-time identity, a wrapping digital counter is usable if elapsed-time subtraction is unsigned and the real observation interval is less than its modulus; a wide enough counter or suitable bounded intervals make this explicit. Software dispatch adds bounded latency, so the specification is achieved to timer/interrupt-service resolution; stricter physical pulse-width requirements call for hardware output comparison or a suitably prioritised timer handler.
Finally, the device register is only a single-bit event latch. Two physical detections before acknowledgement may coalesce, so no software can recover their multiplicity from that bit alone. The event counter must also have enough capacity for the allowed backlog; indefinitely many arrivals faster than one flash every two seconds cannot be guaranteed with finite storage. These are explicit hardware/rate assumptions, not missing input files. Keeping the handler short minimises the rearming interval but cannot abolish those limitations.
Maintain a counter and independent timed finite-state machine for each detector–light pair, while sharing the interrupt scan and timer infrastructure. Blocking for one channel's flash would unnecessarily delay every other channel.
eye_handler():
for i in all_channels:
if EYE_DETECTED[i] != 0:
EYE_DETECTED[i] = 0
pending[i] = pending[i] + 1
main_event_loop():
initialise each LIGHT[i] = 0
initialise pending[i] = 0, state[i] = IDLE
enable detector interrupts
forever:
now = monotonic_timer_snapshot()
for i in all_channels:
service_channel(i, now)
arrange the next wake-up for new work or the earliest deadlineservice_channel is the three-state sequencer from (a), with its own started[i] and an interrupt-safe try_take_event(i). Shared interrupt demultiplexing uses separate if tests, not an else if chain: simultaneous pending detectors must all be acknowledged and counted. If a new detector becomes pending during the scan, the controller must retrigger while a flag remains set or the handler must rescan appropriately; the exact interrupt controller semantics must be checked rather than assuming a single scan can never miss an edge.One hardware timer can schedule the earliest active channel deadline. On its expiry, service every due channel and rearm for the next deadline; a periodic tick with adequate resolution is a simpler alternative. Detectors and the timer can then interleave without serial one-second waits. The handler/main-program counter accesses need the same short critical section protection as in (a). Keep timing state in one execution context, or protect it too if both a timer handler and the main program modify it.
The loop invariants are per channel: the pending count equals recorded but unstarted events,
ON means that channel's light is on, and GAP prevents a new flash until one second after its preceding light-off transition. Different channels may flash simultaneously. The event latch rearming limitation, finite backlog capacity and timer-resolution qualifications apply independently to each channel. These cannot be solved by replacing several event counters with one global Boolean variable.Keep successive timestamps in raw unsigned ticks, and convert their modular difference to milliseconds only after subtraction. The first pulse establishes a timestamp; it does not create a fictitious interval from startup. Store valid intervals in a five-element ring buffer. During startup, average the one to four intervals actually available; return zero until there is at least one valid interval. Here zero is explicitly an unavailable-data sentinel.
The following C programming language code uses the supplied register names through an abstract memory-mapped input-output register interface. The board port supplies the actual register mapping and interrupt vector; no undocumented addresses are assumed. The two interrupt-mask hooks have the exact semantics described below and are interface requirements, not names asserted to exist in an unspecified device library.
#include <stdint.h>
#include <stdbool.h>
struct gpio_registers { volatile uint32_t EVENT; };
struct timer_registers { volatile uint32_t COUNT; };
extern struct gpio_registers GPIO;
extern struct timer_registers TIMER;
uint32_t irq_save_disable(void);
void irq_restore(uint32_t old_mask);
static volatile uint32_t interval_ms[5];
static volatile uint32_t next_slot = 0;
static volatile uint32_t valid = 0;
static volatile uint32_t previous_tick = 0;
static volatile bool have_previous = false;
void gpio_handler(void)
{
if (GPIO.EVENT == 0)
return;
uint32_t now = TIMER.COUNT;
GPIO.EVENT = 0; /* acknowledge with the specified write */
if (have_previous) {
uint32_t ticks = now - previous_tick;
interval_ms[next_slot] = ticks / UINT32_C(16000);
next_slot = (next_slot + 1) % 5;
if (valid < 5)
++valid;
} else {
have_previous = true;
}
previous_tick = now;
}
uint32_t get_pulse(void)
{
uint32_t local[5];
uint32_t mask = irq_save_disable();
uint32_t n = valid;
for (uint32_t i = 0; i < n; ++i)
local[i] = interval_ms[i];
irq_restore(mask); /* restore the earlier mask, not just enable */
if (n == 0)
return 0;
uint64_t total = 0;
for (uint32_t i = 0; i < n; ++i)
total += local[i];
if (total == 0)
return 0;
/* Mean duration is total/n milliseconds; round bpm to nearest integer. */
return (uint32_t)((UINT64_C(60000) * n + total / 2) / total);
}Before the buffer fills, its valid entries occupy positions . Once full, all five positions are valid, so their order is irrelevant to their sum. The conversion uses 16,000 ticks per millisecond. Truncating each interval introduces less than one millisecond of error per interval. The desired conversion is beats per minute, the reciprocal of the mean interval; averaging the individual values is generally different.
irq_save_disable saves the previous interrupt mask and prevents the GPIO handler from running until irq_restore restores that mask. Both hooks must include compiler memory barriers. On an appropriate privileged Cortex-M target, the implementation can use MRS PRIMASK, CPSID i and MSR PRIMASK, with a compiler memory clobber. These target operations implement interrupt masking; they are not expressible as portable ISO C alone. One concrete GCC/Clang-compatible privileged Cortex-M port for those hooks is:uint32_t irq_save_disable(void)
{
uint32_t old_mask;
__asm__ volatile ("mrs %0, primask\n\tcpsid i"
: "=r" (old_mask) : : "memory");
return old_mask;
}
void irq_restore(uint32_t old_mask)
{
__asm__ volatile ("msr primask, %0"
: : "r" (old_mask) : "memory");
}The compiler memory clobbers prevent access motion across the mask changes. Saving and restoring the entire prior mask is intentional; an unconditional enable would break an enclosing critical section.[ref]
CMSIS-Core register access documentation by Arm. PRIMASK, __get_PRIMASK, __disable_irq and __set_PRIMASK
All state is initialized before enabling the peripheral interrupt. Assume the timer read is coherent, the handler is not reentered, and the device follows the stated write-zero acknowledgment semantics.
The interrupt handler is the only writer of
interval_ms, valid and next_slot. The foreground function takes an interrupt-safe ring-buffer snapshot inside a short critical section, copying the count and exactly that many valid entries before restoring the previous mask. It performs the comparatively expensive summation and division after interrupts are restored. A new pulse then affects the next query, while the current query consistently describes the earlier snapshot.Without exclusion, the foreground could read an old entry, be interrupted while the buffer is updated, then read new entries or an inconsistent count. Even indivisible 32-bit loads would not make that multi-element snapshot atomic. The volatile-qualified objects in C ensure that the compiler performs required shared or device accesses;
volatile does not provide mutual exclusion or a coherent multiword snapshot.[ref]C11 committee draft N1570. Sections 5.1.2.3 and 6.7.3, observable volatile accesses and type qualifiers
This design assumes a single-core foreground/interrupt execution model: no other interrupt, DMA engine or core modifies the protected buffer. The mask hooks must prevent compiler reordering across their boundaries and really mask the GPIO interrupt. Restoring the saved mask also makes the function safe to call when interrupts were already disabled. Keep the critical section short enough that the event register does not coalesce multiple heartbeats while interrupts are masked; otherwise the hardware interface cannot reconstruct events it never recorded.
Safety property 2026-10-14
A requirement whose violation has a finite execution prefix that no continuation can repair. Two software threads simultaneously inside a protected critical section already violate mutual exclusion; later leaving does not erase that violation. This contrasts with a liveness property, for which a long finite wait alone does not establish failure.[ref]
Defining Liveness by Bowen Alpern and others. pp. 181–182: finite irremediable prefixes
Volatile-qualified objects in C 2026-10-24
Volatile-qualified objects in C are objects whose accesses must follow the implementation's observable-access rules even when the optimizer cannot see an ordinary program dependency. They are commonly used for device registers and state changed by an interrupt handler, under the target compiler's embedded execution model.
volatile does not by itself provide atomic multiword access, mutual exclusion or inter-thread synchronization. Protecting a shared snapshot still requires a suitable critical section or another synchronization protocol.[ref]