Synchronization primitives: Difference between revisions

From Miosix Wiki
Jump to navigation Jump to search
mNo edit summary
No edit summary
Line 1: Line 1:
Miosix provides a certain number of 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, esepcially device drivers, on Miosix.
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 =
= High level synchronization primitives =
For what concerns high level synchronization primitives, Miosix provides the ususal mutexes and condition variables, both the pthread_* ones and the so called native ones, which are in the form of C++ classes.
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.
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.


== Pthread mutexes and condition variables ==
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.
First of all, unlike most other OSes for microcontrollers, the standard pthread mutexes and condition variables are just a ''#include <pthread.h>'' away. Both the mutex and the condition variable are optimized for speed, for example a lock/unlock pair of function calls to an uncontended ''pthread_mutex_t''' take around 100 clock cycles on an architecture such as the Cortex-M. The implemented policy for threads that lock on both mutexes and condition variables is FIFO to prevent starvation. The timed versions of the pthread functions, such as ''pthread_mutex_timedlock()'' aren't supported yet. Recursive mutexes are supported, though.


== Native mutexes and 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.  
Miosix also provides a C++ oriented set of high level synchronization primitives, the ''Mutex'', ''FastMutex'' and ''ConditionVariable'' classes. ''Mutex'' and ''FastMutex'' have the same public interface, providing the ''lock()'', ''unlock()'' and ''tryLock()'' member functions. The first two require no further comment, the third returns ''true'' if the lock was acquired. The diference between a ''Mutex'' and a ''FastMutex'' is that ''FastMutex'' is exactly as a ''pthread_mutex_t'', while ''Mutex'' provides full support to priority inheritance. However, because of that, it is somewhat slower than a ''FastMutex''. If you're not concerned about priority inheritance (for instance, if all the threads you create have the default priority level), then just use ''FastMutex'', while for realtime tasks, and especially if the mutex is going to be locked for extended periods of time where the order of entrance is important, use ''Mutex''.
 
In addition, the ''Lock<T>'' and ''Unlock<T>'' classes are provided to support RAII-style mutex locking.
Compare this example using directly the ''Mutex'' member functions
Compare this example using directly the ''Mutex'' member functions


Line 72: Line 71:


= Low level synchronization primitives =
= Low level synchronization primitives =
The low level synchronization primitives provide a way to temporarily disable interrupts, or to disable preemption only. They are intended for internal use within the kernel, and for low-level tasks that directly interact with the hardware, such as those involving interrupts.


== Preventing preemption ==
Miosix provides two low-level synchronization primitives, the GlobalIrqLock or "GIL" and the PauseKernelLock or "PK".
This is the first low level synchronization primitive.


Miosix provides the ''pauseKernel()'' and ''restartKernel()'' functions to temporarily disable preemption. When preemption is disabled, interrupts will still be serviced as usual, but the kernel will not preempt the current thread at the end of its timeslice. If the time spent with preemption disabled exceeds the thread's timeslice, a call to ''restartKernel()'' will also yield. Just like with mutexes there's built-in support for RAII with the ''PauseKernelLock'' and ''RestartKernelLock'' classes to help in writing exception-safe code. They work just like the ''Lock<T>'' and ''Unlock<T>'' introduced earlier.
== The GIL 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.
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 [https://en.wikipedia.org/wiki/RAII 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.


Sleeping with ''Thread::sleep()'' or ''usleep()'' is one, as the kernel will try to give the CPU time to some other thread. (An exception are the ''delayMs()'' and ''delayUs()'' functions which are guaranteed to be implemented using busy loops and therefore will work). Locking mutexes, opening files or printing though the standrd output are other ways to get in trouble.
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 help in that, some of the kernel functions are prefixed with PK, such as ''Thread::PKwakeup()''. These functions are guaranteed to be callable when the kernel is paused, and more specifically can '''only''' be called with the kernel paused.
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.


All in all, you won't find much need to use this synchronization primitive, unless you're writing kernel code. At that point, you'll find it useful. A notable use of ''pauseKernel()'' within Miosix is to make the heap thread safe. Whenever you use ''malloc()'' or ''new'', ''pauseKernel()'' is called behind the scenes to protect the heap data structures from corruption. Therefore, a thread is non-preemptable during a call to a memory allocation (or deallocation) function. On the other hand, interrupts will be serviced, preserving real-time performance.
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.


A last important detail is that calls to ''pauseKernel()'' do nest safely. If you call it twice, then you need to call ''restartKernel()'' twice as well for preemption to be enabled again. This is important, as it means that a function that pauses the kernel can call another one that does the same without preemption being enable sooner than expected, which is something you don't want when you're relying on this to perform synchronization. As a direct consequence of this feature, you can safely allocate memory while the kernel is paused.
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.


== Disabling interrupts ==
More importantly, all functions of the kernel that are prefixed with IRQ are '''only''' safe to be called if you're holding the GIL.
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.
Some functions have both an IRQ and non IRQ version, such as ''miosix::getTime()'' and miosix::IRQgetTime()''.


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


Miosix provides four functions to do that: ''fastDisableInterrupts()'', ''fastEnableInterrupts()'', ''disableInterrupts()'' and ''enableInterrupts()''. As always it is strongly recomended to '''not''' use these calls directly, and use the RAII equivalent classes ''FastInterruptDisableLock'', ''FastInterruptEnableLock'', ''InterruptDisableLock'' and ''InterruptEnableLock'' as the compiler will enforce that interrupts will be enabled again at the end of the piece of code you're writing, even if you accidentally insert a return or an exception gets thrown.
If the time spent with preemption disabled exceeds the thread's timeslice a preemption will occur immediately when releasing the lock.


The difference between the fast and the other ones is this: ''fastDisableInterrupts()'' and ''fastEnableInterrupts()'' are inline function that use whatever assembler code the architecture needs to disable interrupts. As they are inline, there's even no function call overhead. In most architectures they introduce just one assembler instruction directly in your code. So you should prefer them whenever you can.
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.


The other two, ''disableInterrupts()'' and ''enableInterrupts()'' are regular function calls, and unlike the fast versions they do nest properly, just like ''pauseKernel()'' does. Moreover, starting from Miosix v1.61, you can both nest ''pauseKernel()'' within ''disableInterrupts()'' and the other way around safely without interrupts being accidentally re-enabled. With the fast version to disable interrupts, this does not work at all. This is important since, as previously said, allocating memory causes pauseKernel() to be called. Therefore, it is now possible to allocate memory with interrupts disabled by an ''InterruptDisableLock'', but not a ''FastInterruptDisableLock''.
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()'', '''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 PK locked are prefixed with PK.


Exactly as when the kernel is paused, you shall not try to cause a preemption while interrupts are disabled, therefore you cannot ''yield()'', ''usleep()'' open files or print.
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 ==
== Summarizing it all ==

Revision as of 15:50, 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() 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(), 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 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 inside a lock to disable interrupts, the other most common case, you will usually not need to call any function, but just modify some of your own data structures.

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 InterruptDisableLock No No No No No Yes * Yes ** Yes ** Yes
Within a FastInterruptDisableLock No No No No No Yes * No No Yes
Within an interrupt service routine No No No No No Yes * No No Yes

* = Most IRQ-prefixed functions can be called both with interrupts disabled (either within an InterruptDisableLock or FastInterruptDisableLock) and within an interrupt routine. A few of them can only be called with interrupts disabled but not within an interrupt routine, and one of them, IRQfindNextThread() which is the scheduler, can only be called inside an interrupt routine but not with interrupts disabled. Refer to the doxygen documentation of the individual function when unsure.

** = Starting from Miosix v1.61