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:

  1. taskA starts running and calls mutex_lock on balanceMutex with WAIT_FOREVER. Since balanceMutex is free, taskA successfully locks it and begins its operations on balance.

  2. While taskA is still running, taskB becomes ready to run and tries to lock balanceMutex. However, it finds that balanceMutex is already locked by taskA. Because WAIT_FOREVER is specified, taskB doesn’t just spin and wait; instead, it yields control back to the RTOS.

  3. 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.

  4. taskA completes its deposit, unlocks balanceMutex, and then either continues with other operations or yields, depending on its design.

  5. When balanceMutex is unlocked, the RTOS resumes taskB, allowing it to acquire the mutex and proceed with its withdrawal. Now, taskB can safely read and modify balance 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: