Mutex
Problem
As an example, can you spot what may go wrong with this snippet, assuming a preemptive RTOS?
int balance = 100;
void taskA() {
int tempA = balance;
tempA += 50; // Deposit 50
balance = tempA;
printf("Task A: Balance = %d\n", balance);
osYield();
}
void taskB() {
int tempB = balance;
tempB -= 30; // Withdraw 30
balance = tempB;
printf("Task B: Balance = %d\n", balance);
osYield();
}
Race Condition
If the RTOS switches from taskA
to taskB
right after taskA
assigns temp = 150
but before updating balance
, tempA
will hold the value 150, but this hasn’t yet been saved to balance
. When taskB
runs, it assigns tempB = 70
and writes that to balance
. Once taskB
completes and yields control back to taskA
, taskA
then updates balance
with its earlier tempA
value of 150, effectively undoing taskB
's withdrawal of 30. This type of problem, where two tasks compete to modify shared data, is known as a race condition.
To counteract this issue, RTOS’s include signalling mechanisms allowing tasks to share data.
Solution
The solution to this issue is pretty straightforward - only let one task at a time modify balance
.
We solve this problem by using a synchronization mechanism called a mutex, short for "mutual exclusion." The purpose of a mutex is to ensure that only one task can access a shared resource, like balance
, at a time. You can think of a mutex like a bathroom "occupied" sign: when someone enters, they flip a sign on the door signalling that the space is in use. Others wait outside until it’s free.
When we use a mutex in code, a task "locks" it before accessing the shared resource and "unlock" it after it's done. This guarantees exclusive access, as only the task holding the mutex can proceed with modifying the resource. If another task tries to enter while locked, it must wait until the first task unlocks it—just like waiting outside the bathroom.
int balance = 100;
mutex_t balanceMutex;
void taskA() {
mutex_lock(&balanceMutex, WAIT_FOREVER);
int temp = balance;
temp += 50; // Deposit 50
balance = temp;
printf("Task A: Balance = %d\n", balance);
mutex_unlock(&balanceMutex);
}
void taskB() {
mutex_lock(&balanceMutex, WAIT_FOREVER);
int temp = balance;
temp -= 30; // Withdraw 30
balance = temp;
printf("Task B: Balance = %d\n", balance);
mutex_unlock(&balanceMutex);
}
Waiting for a Mutex
Notice that the mutex_lock
function has an argument WAIT_FOREVER
- when a task decides to wait for a mutex, it can choose to yield (give up control so other tasks can run) - WAIT_FOREVER
is an arg that says just this. The RTOS will, upon a call to a locked mutex, automatically give control to other tasks, and resume running the calling task once the mutex is free. Here’s an example scenario with the above snippet.
Suppose taskA
is currently running and locks balanceMutex
to perform a deposit. Right after taskA
locks the mutex, taskB
is scheduled to run and tries to lock balanceMutex
as well, but since it’s already held by taskA
, the RTOS handles this situation as follows:
taskA starts running and calls
mutex_lock
onbalanceMutex
withWAIT_FOREVER
. SincebalanceMutex
is free,taskA
successfully locks it and begins its operations onbalance
.While
taskA
is still running, taskB becomes ready to run and tries to lockbalanceMutex
. However, it finds thatbalanceMutex
is already locked bytaskA
. BecauseWAIT_FOREVER
is specified,taskB
doesn’t just spin and wait; instead, it yields control back to the RTOS.RTOS context-switches to other available tasks. If there are other tasks ready to run, the RTOS schedules them. If
taskA
is the only other task, it continues executing.taskA completes its deposit, unlocks
balanceMutex
, and then either continues with other operations or yields, depending on its design.When
balanceMutex
is unlocked, the RTOS resumestaskB
, allowing it to acquire the mutex and proceed with its withdrawal. Now,taskB
can safely read and modifybalance
without interference.
Ignoring a mutex
Instead of waiting forever, a task also has the option of specifying a timeout on how long it wants to wait for a mutex to become available. Imagine a scenario where we want to wait 2 units of time after the mutex locks - the snippet is as follows
int balance = 100;
mutex_t balanceMutex;
#define WAIT_TIME_MUTEX_TICKS 2U
void taskA() {
mutex_lock(&balanceMutex, WAIT_TIME_MUTEX_TICKS);
int temp = balance;
temp += 50; // Deposit 50
balance = temp;
printf("Task A: Balance = %d\n", balance);
mutex_unlock(&balanceMutex);
}
void taskB() {
mutex_lock(&balanceMutex, WAIT_TIME_MUTEX_TICKS);
int temp = balance;
temp -= 30; // Withdraw 30
balance = temp;
printf("Task B: Balance = %d\n", balance);
mutex_unlock(&balanceMutex);
}
In this code snippet, even though we use a mutex and call functions to lock and unlock it, we aren’t verifying if the mutex was actually locked successfully in either task. A mutex alone doesn’t ensure synchronization—it simply allows tasks to signal when they don’t want other tasks interfering with a shared resource (like balance here). To use the bathroom sign analogy: even if a sign indicates the bathroom might be occupied, someone could ignore the sign and walk in (eww…). Similarly, if an engineer neglects to check the status of a mutex and lock/unlock it before accessing shared resources, it’s essentially as if the mutex isn’t even there.
The correct way to do this is something like this: