Synchronization primitives

From Miosix Wiki
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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.

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.

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

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

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

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

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

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.

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.

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.

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.

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.

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.

Disabling interrupts

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.

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.

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.

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.

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.

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