The synchronization is the coordination of access to shared data, peripherals, and events among multiple execution contexts.
Imagine two threads trying to write to the same global variable, or send data over the same I2C port at the same time. Who wins? Do the data get mixed up?
What happens is a disaster known as a Race Condition. Data gets corrupted and the program fails randomly, making it difficult to debug.
To avoid this, Zephyr provides us with synchronization mechanisms. Today we will look at the two most important ones: the Mutex and the Semaphore.
The problem: the race condition
Let’s visualize the problem with a classic example. Imagine a global variable counter = 0.
- Thread A wants to increment the counter (
counter++). - Thread B wants to increment the counter (
counter++).
If they both do it at the same time, we might think the result will be 2. But in assembly, counter++ is three steps:
Read the memory value into the CPU register.
Add 1 to the register.
Write the register back to memory.
If Thread A is interrupted by Thread B right after step 1, both threads will read the counter as 0. Both will add 1, and both will write 1. Result: 1 (instead of 2). We have lost data.
Any shared resource (global variables, buffers, hardware peripherals like UART/I2C) must be protected if more than one thread will access it.
The solution: mutex (mutual exclusion)
A Mutex (Mutual Exclusion) works like the key to a gas station restroom. There is only one key.
- If you want to enter, you ask for the key (
lock). - If it’s not available, you wait at the door until the person inside leaves.
- When you’re done, you return the key (
unlock) so the next person can enter.
In Zephyr, a Mutex ensures that only one thread can access a critical section of code at a time.
Implementation in Zephyr
Let’s protect our counter using a Mutex.
#include <zephyr/kernel.h>
/* 1. Define the Mutex statically */
K_MUTEX_DEFINE(my_mutex);
int shared_counter = 0;
void increment_counter(void)
{
/* 2. Try to take the Mutex */
/* K_FOREVER means: "if it's busy, put me to sleep until it's free" */
k_mutex_lock(&my_mutex, K_FOREVER);
/* --- START CRITICAL SECTION --- */
/* In here we are alone. No one else can enter. */
shared_counter++;
printk("Counter: %d\n", shared_counter);
/* --- END CRITICAL SECTION --- */
/* 3. Release the Mutex (MANDATORY!) */
k_mutex_unlock(&my_mutex);
}Beware of deadlocks. If a thread holds a mutex, or multiple threads acquire them in a different order, the involved threads can wait indefinitely. Release the mutex on every exit path and maintain a stable acquisition order.
Reentrant mutex
A great feature of Mutexes in Zephyr is that they are reentrant. If the thread that already has the key tries to take it again, the system lets it pass (it simply increments an internal counter). This prevents a thread from accidentally blocking itself.
Semaphores (k_sem)
If a Mutex is a key, the Semaphore is a counter of available spaces in a parking lot. Or a notification mechanism.
Unlike the Mutex, the Semaphore has no owner. One thread can take it (decrement) and a different one can give it (increment). This makes it ideal for synchronizing events (e.g., “Data has arrived”).
Use case: producer-consumer
Imagine a Timer (which we learned about in the previous article) that reads a sensor every second. Since the Timer cannot process complex data, it notifies a Thread to do so.
#include <zephyr/kernel.h>
/* Define a semaphore with initial value 0 and maximum value 1 */
K_SEM_DEFINE(my_sem, 0, 1);
/* Timer: The Producer */
void my_timer_handler(struct k_timer *dummy)
{
/* "Give" the semaphore (Increment its value) */
/* This notifies whoever is waiting */
k_sem_give(&my_sem);
}
/* Thread: The Consumer */
void processing_thread(void *p1, void *p2, void *p3)
{
while (1) {
/* "Take" the semaphore. */
/* If the value is 0, the thread sleeps here until the timer does 'give' */
k_sem_take(&my_sem, K_FOREVER);
/* If we reach here, the timer has fired */
printk("Event received! Processing data...\n");
}
}In this example, the thread processing_thread does not waste CPU. It is asleep (Suspended) until the semaphore turns green. This is the most efficient way to coordinate tasks.
Mutex vs semaphore
It’s easy to confuse them. The practical difference is this:
| Feature | Mutex (k_mutex) | Semaphore (k_sem) |
|---|---|---|
| Purpose | Protect resources (Exclusion) | Signal events / Limit access |
| Concept | Single key | Resource counter |
| Owner | Yes (whoever locks must unlock) | No (anyone can give/take) |
| Typical use | Access to global variables, I2C, UART | ISR notifies Thread, Synchronization |
| Priority | Supports Priority Inheritance | Does not alter priorities |
Priority Inheritance: Zephyr is clever. If a High-Priority thread is waiting for a Mutex held by a Low-Priority thread, Zephyr temporarily raises the priority of the low-priority thread so it can finish quickly and release the Mutex. This avoids the dreaded “Priority Inversion”.