Synchronization internals -- the mutex
This two-part series addresses the use and misuse of two of the most essential synchronization primitives in modern embedded systems, the mutex (this part) and the semaphore (part 2). Mutexes and semaphores are provided by most current RTOSs, although they can be implemented on bare-metal systems as well. Their use is considered a best practice among experienced firmware engineers, because they can make code more intuitive, leading to better maintainability, improved logic flow and higher productivity. If you’ve been using flags, variables and super loops to react to changes and control events in your code, read on to learn how skillful use of mutexes, semaphores and threads can improve your work.
Practically all desktop operating systems and most real-time operating systems (RTOSs) include APIs for manipulating mutexes and semaphores. They’re often referred to as synchronization objects because they can be used to coordinate or protect access to resources (i.e., variables, code sections, functions, hardware) among two or more threads of execution. They are also especially well-suited to manage triggers and signal events in an application. A thread of execution could be an interrupt service routine (ISR) or a traditional background process or task.
If you’re not using a modern RTOS (e.g., FreeRTOS, ChibiOS, MQX, uC/OS-III, ThreadX, QNX, Linux, etc.) in your firmware designs, consider incorporating one just to get access to these two important tools. They’re that useful!
A commonly held misconception is that a mutex is the same as a “binary” semaphore. To put it differently, the oft-quoted but incorrect belief is that a “counting” semaphore with an initial value of 1 is functionally the same as a mutex. The truth is actually quite different, and understanding the differences will enable you to produce programs that integrate better with your RTOS and avoid CPU waste.
In general, mutexes are used to protect individual resources. They function much like a lock, and have the concept of an owner. By contrast, semaphores contain a built-in counter and are typically used to coordinate access to one or more resources, or execution among two or more processes. A semaphore isn’t necessarily owned by a single process because it coordinates with two or more such processes. Because of the counter, semaphores are sometimes called counting semaphores. A semaphore which restricts its counter to 0 or 1 is known as a binary semaphore.
As suggested above, mutexes support just two basic operations, lock() and unlock(), which are self-explanatory. Semaphores are a bit more abstract, but similarly support just two major operations – take() (aka increment) and give() (aka decrement). For historical reasons, the semaphore take() operation is sometimes confusingly referred to by the capital letter “P”, and the give() operation by the capital letter “V”. You may see these abbreviations used in flowcharts and functional descriptions. We’ll delve into the meaning of take() and give() later in Part 2 of this series.
Inversion of Control
When dealing with an RTOS, it’s essential to understand that your code is not in charge. That’s right! You’re not in charge – you have no idea of when or if or for how long your code gets to run. The RTOS’s scheduler routine is the one calling the shots, which decides what code to run and when to run it.
Only one task at a time can run in a CPU core, and the scheduler coordinates access to it among multiple processes, or tasks. It allows tasks to temporarily execute for a short period, and then suspend so that other tasks can have a chance to execute. This is called a task switch. Because the scheduler is able to quickly suspend and resume the code of multiple tasks, using a high-speed timer called a system tick, it is able to give the appearance of simultaneous multitasking, something known as pre-emptive multitasking.
This leads to a concept known as inversion of control, which means that the scheduler is in charge of choosing when to suspend and resume your code as other tasks are quickly switched in and out. Using a variety of factors, such as task priority, the scheduler automatically decides what to run next. Mutexes and semaphores give us the ability to (a) conditionally suspend a task, and (b) influence how the scheduler chooses the next task to execute.