Synchronization primitives: Difference between revisions
No edit summary |
mNo edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 86: | Line 86: | ||
There's nothing new in that, if you've programmed microcontrollers before without an OS, you've accustomed to disabling interrupts as a mean of synchronization. The only difference is that in multicore architectures, you'll have to remember to take the ''FastGlobalLockFromIrq'' also from within interrupt handlers before calling the scheduler API to wakeup tasks, as the scheduler could be running on another core causing a race condition. In single core architectures you don't technically need to put a ''FastGlobalLockFromIrq'' in interrupt handlers. If you do it will be optimized away as it's not required. Since there's no performance penalty, you may as well take the habit of always using ''FastGlobalLockFromIrq'', just in case your driver gets reused in a multicore chip. | There's nothing new in that, if you've programmed microcontrollers before without an OS, you've accustomed to disabling interrupts as a mean of synchronization. The only difference is that in multicore architectures, you'll have to remember to take the ''FastGlobalLockFromIrq'' also from within interrupt handlers before calling the scheduler API to wakeup tasks, as the scheduler could be running on another core causing a race condition. In single core architectures you don't technically need to put a ''FastGlobalLockFromIrq'' in interrupt handlers. If you do it will be optimized away as it's not required. Since there's no performance penalty, you may as well take the habit of always using ''FastGlobalLockFromIrq'', just in case your driver gets reused in a multicore chip. | ||
Inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including ''printf()'' and filesystem-related functions). Also, the only functions that are part of the Miosix kernel API that are safe to be called with the GIL locked are prefixed with IRQ. Why IRQ and not GIL you may say? It's because of older version of the kernel, before multicore support was introduced. | Inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including ''printf()'', '''malloc()/new''' and filesystem-related functions). Also, the only functions that are part of the Miosix kernel API that are safe to be called with the GIL locked are prefixed with IRQ. Why IRQ and not GIL you may say? It's because of older version of the kernel, before multicore support was introduced. | ||
More importantly, all functions of the kernel that are prefixed with IRQ are '''only''' safe to be called if you're holding the GIL. | More importantly, all functions of the kernel that are prefixed with IRQ are '''only''' safe to be called if you're holding the GIL. | ||
| Line 99: | Line 99: | ||
Despite faster than a mutex, these synchronization primitives carry some heavy restrictions, and are mostly used to implement parts of the kernel itself. The main isssue is that you must never force a preemption when the kernel is paused. If this happens, the kernel will likely lock up badly. Of course, this means you can't call ''Thread::yield()'' when the kernel is paused, but there are many more subtler ways to cause a preemption. | Despite faster than a mutex, these synchronization primitives carry some heavy restrictions, and are mostly used to implement parts of the kernel itself. The main isssue is that you must never force a preemption when the kernel is paused. If this happens, the kernel will likely lock up badly. Of course, this means you can't call ''Thread::yield()'' when the kernel is paused, but there are many more subtler ways to cause a preemption. | ||
In general, inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including ''printf() | In general, inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including ''printf()'' and filesystem-related functions). Also, the only functions that are part of the Miosix kernel API that are safe to be called with the PK locked are prefixed with PK. | ||
All in all, you won't find much need to use the PK synchronization primitive, unless you're writing kernel code. At that point, you'll find it useful. Miosix uses it internally to make regular mutexes and condition variables work. | All in all, you won't find much need to use the PK synchronization primitive, unless you're writing kernel code. At that point, you'll find it useful. Miosix uses it internally to make regular mutexes and condition variables work. | ||
| Line 106: | Line 106: | ||
The following table summarizes what you can do depending on what low level synchronization primitive you are using, as well as when you are writing the code of an interrupt service routine. | The following table summarizes what you can do depending on what low level synchronization primitive you are using, as well as when you are writing the code of an interrupt service routine. | ||
Although it may seem complicated, it is because it contains all possible cases, most of which are rather uncommon. In most cases you'll be writing code while not being into the scope of any low level lock, so you can do everything but call functions prefixed with IRQ or PK. When you are within an interrupt or | Although it may seem complicated, it is because it contains all possible cases, most of which are rather uncommon. In most cases you'll be writing code while not being into the scope of any low level lock, so you can do everything but call functions prefixed with IRQ or PK. When you are within an interrupt or take the GIL, the other most common case, you will usually not need to call any function besides the kernel API prefixed with IRQ. | ||
{| class="wikitable" | {| class="wikitable" | ||
| Line 142: | Line 142: | ||
| style="background-color: #80ff80;" | Yes | | style="background-color: #80ff80;" | Yes | ||
|- | |- | ||
| Within an | | Within an GlobalIrqLock | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| Line 148: | Line 148: | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| style="background-color: # | | style="background-color: #80ff80;" | Yes | ||
| style="background-color: # | | style="background-color: #ff8080;" | No ** | ||
| style="background-color: # | | style="background-color: #ff8080;" | No ** | ||
| style="background-color: #80ff80;" | Yes | | style="background-color: #80ff80;" | Yes | ||
|- | |- | ||
| Within a | | Within a FastGlobalIrqLock | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| Line 164: | Line 164: | ||
| style="background-color: #80ff80;" | Yes | | style="background-color: #80ff80;" | Yes | ||
|- | |- | ||
| Within an interrupt service routine | | Within an interrupt service routine (taking FastGlobalLockFromIrq in the multicore case) | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| style="background-color: #ff8080;" | No | | style="background-color: #ff8080;" | No | ||
| Line 176: | Line 176: | ||
|- | |- | ||
|} | |} | ||
<nowiki>*</nowiki> = Most IRQ-prefixed functions can be called both with interrupts disabled (either within an '' | <nowiki>*</nowiki> = Most IRQ-prefixed functions can be called both with interrupts disabled (either within an ''GlobalIrqLock'', ''FastGlobalIrqLockLock'' or ''FastGlobalLockFromIrq'') and within an interrupt routine. The only exception being ''IRQregisterIrq'' and ''IRQunregisterIrq'' which are used for registering/unregistering interrupts, that cannot be called from within an interrupt handler and forcedly require a ''GlobalIrqLock''. Refer to the doxygen documentation of the individual function when unsure. | ||
<nowiki>**</nowiki> = | <nowiki>**</nowiki> = Used to be possible from Miosix v1.61 to Miosix v2.81, no longer possible since Miosix v2.99 due to multicore support. Now that a core can take the GIL and another core the PK, we must enforce that taking both locks can only be done by first taking the PK and then "upgrading" to the GIL. Doing the reverse would cause deadlocks, and since malloc/new take the PK, it's no longer possible to first take the GIL and then malloc. | ||
[[Category:API]] | [[Category:API]] | ||
Latest revision as of 16:02, 15 May 2026
Miosix provides synchronization primitives, both high and low level. High level synchronization primitives are standard mutexes and condition variables that are familiar to programmers of multithreaded applications. This page addresses in more detail the low level synchronization primitives which are Miosix-specific, and is most useful when developing low level code, especially device drivers, on Miosix.
High level synchronization primitives
For what concerns high level synchronization primitives, Miosix provides the ususal mutexes and condition variables, accessible through three APIs:
- The C++11 classes such as std::thread, std::mutex and std::condition_variable through the C++ standard library
- The POSIX thread functions and opaque data types such as pthread_t, pthread_mutex_t and pthread_cond_t through the C standard library
- The native Miosix API which is the underlying implementation of all the other APIs. This is a C++ API including class Thread which is the actual Task Control Block (TCB) used by the scheduler, and classes Mutex, FastMutex, KernelMutex and ConditionVariable
The use for these synchronization primitives is to protect data structures from concurrent access by multiple threads. These synchronization primitives are very easy to use, with almost no preconditions or special requirements, but cannot be used for more low-level tasks, such as synchronizing between threads and interrupt routines.
Both the mutex and the condition variable are optimized for speed, for example a lock/unlock pair of function calls to an uncontended mutex take around 100 clock cycles on an architecture such as the Cortex-M. Recursive mutexes are supported, and so are mutexes with priority inheritance, as well as timed waits on condition variables.
Just like the standard C++, RAII-style mutex locking is recommended also with the native API, through the provided Lock<T> and Unlock<T> classes. Compare this example using directly the Mutex member functions
class Foo
{
int x;
Mutex m;
public:
void decIfPositive()
{
m.lock();
if(x<=0) return;
x--;
m.unlock();
}
};
As you can see, the code contains an error: the return in the middle of the function does not unlock the mutex, something that will likely cause problems.
On the other hand, if a Lock' is used
class Foo
{
int x;
Mutex m;
public:
void decIfPositive()
{
Lock<Mutex> l(m);
if(x<=0) return;
x--;
}
};
you don't have to worry about that. When considering that functions can return due to a propagation of a C++ exception, using locks is the only way to write exception-safe C++ code.
If you need to temporarily unlock a mutex from within a scope where a Lock is in effect, this is made possible by creating an inner scope and using an Unlock object, like this:
Mutex m;
{
Lock<Mutex> l(m);
//Now the Mutex is locked
{
Unlock<Mutex> u(l);
//Now the mutex is unlocked
}
//Now the Mutex is again locked
}
Note how to create an Unlock object you have to pass a Lock to its constructor. This prevents you from trying to unlock a mutex you have not locked.
For what concerns the ConditionVariable class, it provides the wait(), signal() and broadcast() member functions as you'd expect. Moreover, there's an overload of wait() taking a Lock instead of a bare mutex for convenience.
Low level synchronization primitives
Miosix provides two low-level synchronization primitives, the GlobalIrqLock or "GIL" and the PauseKernelLock or "PK".
The GIL lock
The GlobalIrqLock or "GIL". On single core architectures, this lock disables interrupts and makes the code fragment taking this lock uninterruptible, not even by interrupt handlers. On multicore architectures, this lock disables interrupts on the core taking the lock, making the code fragment using it uninterruptible, not even by interrupt handlers. Additionally, only up to one core can take the GIL at any given time. If a second core attempts to take the GIL while another core has already taken it, it will block execution until the core holding the GIL has released it. Taking the GIL must be done using one of three RAII C++ classes: GlobalIrqLock, which is recursive, thus allowing to take the lock multiple times, FastGlobalIrqLock, a faster but non-recursive version, or FastGlobalLockFromIrq, the only version that can be used to take the GIL from an interrupt handler (this version is not recursive).
The GIL is used to implement device drivers that make use of interrupts, as well as inside the kernel, by the scheduler which also runs from inside an interrupt. Since both use the same lock, it's possible for an interrupt handler in a device driver to wakeup a thread from within an interrupt handler without corrupting the scheduler data structures.
The reason why a regular mutex cannot be used for this purpose is because interrupts are by their very nature asymmetric. An interrupt can interrupt the code of a thread, but a thread cannot interrupt an interrupt. This means that when you're writing device drivers and you need to synchronize between an interrupt and normal code a mutex won't do the job. If the mutex is already locked when the interrupt code starts, trying to lock it within the interrupt will attempt to block the interrupt till the thread unlocks it, and as you can imagine, this won't end up well.
To synchronize in such cases you have to temporarily disable interrupts from within your thread or main program, so that the thread and the interrupt won't try to access some data structure at the same time.
There's nothing new in that, if you've programmed microcontrollers before without an OS, you've accustomed to disabling interrupts as a mean of synchronization. The only difference is that in multicore architectures, you'll have to remember to take the FastGlobalLockFromIrq also from within interrupt handlers before calling the scheduler API to wakeup tasks, as the scheduler could be running on another core causing a race condition. In single core architectures you don't technically need to put a FastGlobalLockFromIrq in interrupt handlers. If you do it will be optimized away as it's not required. Since there's no performance penalty, you may as well take the habit of always using FastGlobalLockFromIrq, just in case your driver gets reused in a multicore chip.
Inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including printf(), malloc()/new and filesystem-related functions). Also, the only functions that are part of the Miosix kernel API that are safe to be called with the GIL locked are prefixed with IRQ. Why IRQ and not GIL you may say? It's because of older version of the kernel, before multicore support was introduced.
More importantly, all functions of the kernel that are prefixed with IRQ are only safe to be called if you're holding the GIL.
Some functions have both an IRQ and non IRQ version, such as miosix::getTime() and miosix::IRQgetTime().
The PK lock
The PauseKernelLock or "PK". This lock disables preemption. This means that the code fragment taking the lock cannot be preempted by other threads, but can be interrupted by interrupts. On multicore architectures, only up to one core can take the PK at any given time. If a second core attempts to take the PK while another core has already taken it, it will block execution until the core holding the PK has released it. Note that on multicore architectures, it is allowed for one core can take the GIL, while another core can take the PK.
If the time spent with preemption disabled exceeds the thread's timeslice a preemption will occur immediately when releasing the lock.
Despite faster than a mutex, these synchronization primitives carry some heavy restrictions, and are mostly used to implement parts of the kernel itself. The main isssue is that you must never force a preemption when the kernel is paused. If this happens, the kernel will likely lock up badly. Of course, this means you can't call Thread::yield() when the kernel is paused, but there are many more subtler ways to cause a preemption.
In general, inside a critical section taking the GIL you should not call functions that cause a preemption, such as almost all high-level function form the C++ standard library (including printf() and filesystem-related functions). Also, the only functions that are part of the Miosix kernel API that are safe to be called with the PK locked are prefixed with PK.
All in all, you won't find much need to use the PK synchronization primitive, unless you're writing kernel code. At that point, you'll find it useful. Miosix uses it internally to make regular mutexes and condition variables work.
Summarizing it all
The following table summarizes what you can do depending on what low level synchronization primitive you are using, as well as when you are writing the code of an interrupt service routine.
Although it may seem complicated, it is because it contains all possible cases, most of which are rather uncommon. In most cases you'll be writing code while not being into the scope of any low level lock, so you can do everything but call functions prefixed with IRQ or PK. When you are within an interrupt or take the GIL, the other most common case, you will usually not need to call any function besides the kernel API prefixed with IRQ.
| file related operations | printf, scanf, cin, cout | Thread::yield(), Thread::sleep(), usleep() | call kernel functions with no prefix | call kernel functions with PK prefix | call kernel functions with IRQ prefix | allocate memory | throw C++ exceptions | delayMs() and delayUs() | |
| Normal code | Yes | Yes | Yes | Yes | No | No | Yes | Yes | Yes |
| Within a PauseKernelLock | No | No | No | No | Yes | No | Yes | Yes | Yes |
| Within an GlobalIrqLock | No | No | No | No | No | Yes | No ** | No ** | Yes |
| Within a FastGlobalIrqLock | No | No | No | No | No | Yes * | No | No | Yes |
| Within an interrupt service routine (taking FastGlobalLockFromIrq in the multicore case) | No | No | No | No | No | Yes * | No | No | Yes |
* = Most IRQ-prefixed functions can be called both with interrupts disabled (either within an GlobalIrqLock, FastGlobalIrqLockLock or FastGlobalLockFromIrq) and within an interrupt routine. The only exception being IRQregisterIrq and IRQunregisterIrq which are used for registering/unregistering interrupts, that cannot be called from within an interrupt handler and forcedly require a GlobalIrqLock. Refer to the doxygen documentation of the individual function when unsure.
** = Used to be possible from Miosix v1.61 to Miosix v2.81, no longer possible since Miosix v2.99 due to multicore support. Now that a core can take the GIL and another core the PK, we must enforce that taking both locks can only be done by first taking the PK and then "upgrading" to the GIL. Doing the reverse would cause deadlocks, and since malloc/new take the PK, it's no longer possible to first take the GIL and then malloc.