Chapter 6 — Thread Safety

6.1 Why It Matters

Even on bare-metal systems without an OS, the library operates in two concurrent contexts:

Without synchronization, the ISR could interrupt the main loop in the middle of a buffer pool operation, corrupting the data structure.

6.2 The Lock/Unlock Contract

The library defines a lock/unlock contract through callback function pointers that the application must provide:

// Application provides these functions:
void lock_shared_resources(void);    // Enter critical section
void unlock_shared_resources(void);  // Leave critical section

The library calls lock_shared_resources() before accessing any shared data structure and unlock_shared_resources() immediately after. The critical section must be as short as possible to minimize interrupt latency.

6.3 What Must Be Protected

flowchart TB subgraph SHARED["Shared Resources (require lock)"] A["CAN Buffer Store
(allocate/free)"] B["CAN FIFO
(push/pop)"] C["Alias Mapping Table
(is_duplicate flag)"] D["Listener Alias Table
(alias resolution)"] E["OpenLCB Buffer List
(timeout check)"] end subgraph ISR["ISR Context (writes)"] I1["CAN RX: allocate + push"] I2["CAN RX: set is_duplicate"] I3["CAN RX: update listener alias"] end subgraph MAIN["Main Loop (reads/writes)"] M1["CAN RX SM: pop + free"] M2["CAN TX SM: allocate"] M3["Main SM: check duplicates"] M4["Main SM: resolve aliases"] M5["Main SM: check timeouts"] end I1 --> A I1 --> B I2 --> C I3 --> D M1 --> A M1 --> B M2 --> A M3 --> C M4 --> D M5 --> E style SHARED fill:#fff3e0,stroke:#e65100 style ISR fill:#ffcdd2,stroke:#c62828 style MAIN fill:#c8e6c9,stroke:#2e7d32
ResourceISR AccessMain Loop AccessWhy Lock Needed
CAN Buffer Store allocate_buffer() allocate_buffer(), free_buffer() Concurrent allocation could return same slot twice
CAN FIFO push() pop() Concurrent push/pop could corrupt head/tail pointers
Alias Mapping Table Set is_duplicate flag Read and clear is_duplicate Flag read-modify-write is not atomic
Listener Alias Table Write alias on AMD arrival Read alias for TX resolution Partial write visible to reader
OpenLCB Buffer List None check_timeouts() Documentation requires lock (future-proofing)

6.4 Bare-Metal Implementation

On bare-metal systems, the simplest and most reliable approach is to disable and re-enable interrupts:

// dsPIC example
void lock_shared_resources(void) {
    __builtin_disi(0x3FFF);  // Disable interrupts
}

void unlock_shared_resources(void) {
    __builtin_disi(0x0000);  // Re-enable interrupts
}

// ARM Cortex-M example
void lock_shared_resources(void) {
    __disable_irq();
}

void unlock_shared_resources(void) {
    __enable_irq();
}

// ESP32 (FreeRTOS) example
static portMUX_TYPE spinlock = portMUX_INITIALIZER_UNLOCKED;

void lock_shared_resources(void) {
    portENTER_CRITICAL(&spinlock);
}

void unlock_shared_resources(void) {
    portEXIT_CRITICAL(&spinlock);
}

6.5 RTOS Implementation

On an RTOS, use a mutex or critical section:

// FreeRTOS example with mutex
static SemaphoreHandle_t shared_mutex;

void lock_shared_resources(void) {
    xSemaphoreTake(shared_mutex, portMAX_DELAY);
}

void unlock_shared_resources(void) {
    xSemaphoreGive(shared_mutex);
}
ISR Context Warning

If the lock function is called from both ISR and main loop context, a standard mutex will not work. Use a critical section (disable interrupts) or an ISR-safe mutex. On FreeRTOS, portENTER_CRITICAL() / portEXIT_CRITICAL() is the appropriate mechanism when ISR and task contexts share resources.

6.6 Common Pitfalls

PitfallSymptomFix
Forgetting to lock around CAN FIFO push in ISR Corrupted FIFO, random crashes, lost messages Wrap ISR push in lock/unlock
Holding lock for too long Increased interrupt latency, missed CAN frames at high bus load Keep critical sections minimal -- only the shared access
Using a non-ISR-safe mutex from ISR context Deadlock or undefined behavior Use disable-interrupts or ISR-safe primitives
Nested locks without nesting support Premature unlock on inner unlock call Ensure lock implementation supports nesting, or restructure code
← Previous: Ch 5 — Node Management Next: Ch 6b — Callback Interface Pattern →