Mutex for task scheduler.

A couple of days ago there was a short discussion on the task scheduler library ( http://arduino.cc/forum/index.php/topic,130917.0.html ).
One aspect of the library which hasn't been implemented yet was a mutex or similar form of mutual exclusion; as the task scheduler library is something that I would see myself using, and keeping with the spirit of open source I thought I would give one a try. This library is a little strange, I did not want to modify the task scheduler until I had a stable idea, also to make it easier for people to test without rummaging through the Arduino core. As a result the library is fully compatible with UNO and other 8 bitters.

Also, as I do not have a DUE to test anything, I have provided a sketch at the bottom which would be great if someone could collect the serial output for me :slight_smile:

It has fully global counters, It doesn't suffer from the problems known to things like COUNTER.
Locks are pretty much inaccessible to users, they cannot be manually created, compile time errors will occur if attempted.
Locks are implemented as types rather than values, this guarantees a lock is globally unique, not just to a translation unit.
An update I'll have shortly allows mutex data to be non-volatile if you guarantee it is exclusive to the task scheduler ( not shared with ISR's ).

Here is the basic layout:

atomic_mode can be one of the three options below:

  • m_Restore
  • m_Force
  • m_None

HMUTEX is a handle to a mutex.
HLOCK is a handle to a lock.
HTASK is a handle to a task.

User interface:

  • createMutex( atomic_mode )
    Creates a mutex based on a blocking mode.
    Returns a handle to a mutex.

  • createMutexEx( atomic_mode, lock_mode )
    Creates a mutex based on a blocking mode, also specify what sort of lock to use.
    Locks can be either singular or recursive, using these input values 'l_Simple' or 'l_Recursive'
    Returns a handle to a mutex.

  • duplicateHandle( mutex_handle, atomic_mode )
    Copies a handle, more specifically, allows a lock to be used by more than one mutex with different blocking methods.
    Returns a handle to a mutex.

  • acquireMutex( mutex_handle )
    Use to lock a mutex, returns a boolean specifying success or failure.

  • releaseMutex( mutex_handle )
    Use to unlock a mutex, returns a boolean specifying success or failure.

  • getOwner( mutex_handle )
    Returns the ID of the task that has locked the mutex. LOCK_FREE is returned if the mutex is unlocked.

  • getMutexMode( mutex_handle )
    Returns the blocking method of the Mutex 'mutex_handle' refers to.

  • registerTask( name )
    Use to notify the system of functions that are independent tasks ( interrupts & scheduler tasks ).
    All registered tasks must be marked with the function modifier 'TASK'
    This returns a handle to a task( they are not useful yet ).

  • isOwner( mutex_handle )
    Returns true if the calling task owns the mutex.

  • getTaskID()
    Returns the ID of the calling task.

  • safeCall()
    Allows calling task functions from within a task.

I'll have more time after work to be clearer in my explanation if needed.

Here is a test sketch, I don't have a DUE so I could only compile it. ( I would just like to see the output of the task ID's, or if this will even run in its current state )

  #include "Mutex.h"
  
  #ifdef __arm__
    #include <Scheduler.h>
  #endif

  //Two mutex's created, and two handles duplicated.
  HMUTEX h_MutexA0 = createMutex( m_Force );                  //Uses atomic blocking.
  HMUTEX h_MutexA1 = duplicateHandle( h_MutexA0, m_Restore ); //Uses atomic blocking in restore mode ( initial interrupt mode may be unknown ).
  HMUTEX h_MutexB0 = createMutex( m_None );                   //No atomic blocking. ( read TC0_Handler )
  HMUTEX h_MutexB1 = duplicateHandle( h_MutexB0, m_Force );   //Uses atomic blocking.
  
  HTASK h_TaskA = registerTask( loop_1 );
  HTASK h_TaskB = registerTask( TC0_Handler );

  void setup(){ 
    #ifdef __arm__
      Scheduler.startLoop( loop_1 );
      //Need to setup stuff for TC0_Handler, I do not know how to setup ARM interrupts yet.
    #endif
    Serial.begin(9600);
      
  }

  void loop(){ 
    Serial.println( "================================" );
    if( acquireMutex( h_MutexA0 ) ){
      //some shared memory operation.
     releaseMutex( h_MutexA0 );
    }
    Serial.print( "loop ID: " );
    Serial.println( getTaskID(), HEX );
    foo();
    
    //Simulate scheduler call for AVR8
    #ifndef __arm__
      loop_1();
    #endif
    
    //Call task within another task
    Serial.print( "safeCall a task within task: " );
    Serial.println( getTaskID(), HEX );
    safeCall( loop_1 );
    delay(3000);
  }
    
  _TASK_ void loop_1(){

    if( acquireMutex( h_MutexA1 ) ){
      releaseMutex( h_MutexA1 );
    }
    Serial.print( "loop_1 ID: " );
    Serial.println( getTaskID(), HEX ); 
    foo();
    #ifdef __arm__
      delay(500);
      yield();
    #endif    
  }  
  
  _TASK_ void TC0_Handler(){
    //h_MutexB0 does not use atomic blocking, i'm assuming like the AVR, interrupts are disabled on entry to an isr ( could be wrong, just for example ).
    if( acquireMutex( h_MutexB0 ) ){
      releaseMutex( h_MutexB0 );
    } 
    delay(10);
    Serial.print( "IRQ_Handler ID: " );
    Serial.println( getTaskID(), HEX );  
  }   
  
  void foo(){
    if( acquireMutex( h_MutexB1 ) ){
      releaseMutex( h_MutexB1 );
    }     
    Serial.print( "foo ID: " );
    Serial.println( getTaskID(), HEX );      
  }

It requires my AtomicBlock library which is covered in detail here: Replacement for AVR libc ATOMIC_BLOCK macros, now for DUE and other platforms. - Libraries - Arduino Forum

Files are available below or on Google code here if you aren't signed into Arduino.
Note: Google code documents are marked with revision numbers, the test code above will need the include changed to "Mutex_01b.h", or you can change the file names to suit.

AtomicBlock_121b.h (21 KB)

Mutex.h (11.2 KB)

Mutex.cpp (92 Bytes)

Why not do a well designed kernel instead of growing it by software sprawl. We have yield(), wait(), maybe Time-Triggered Cooperative (TTC) scheduling, a mutex, a way to wake a thread from an isr. Finally you will have the functionality of a true OS.

I started my career developing an OS for early multiprocessor supercomputers, CDC 6600 and others, in the late 1960s. I have watched this haphazard growth of the OS happen at each level of computer, mainframe, minicomputer, PC, single board computer, single chip processors, ....

Now Arduino is doing the same drill.

Why?

As a result the library is fully compatible with UNO and other 8 bitters.

Are you saying that we can use the scheduler library with any board?

Patouf:
Are you saying that we can use the scheduler library with any board?

No ( hopefully soon though ), the mutex library is completely self contained. It is not reliant on an OS or the task scheduler. The test code posted above was tested on my UNO, the task scheduler is utilised when compiled for the DUE. The mutex works by hi-jacking the task scheduler's context changes and system interrupts. In reference to 8-bitters, this current setup is suitable for locking shared data between the main app flow and/or ISR's.

@fat16lib

I agree, a layered approach adds in redundancies, my mutex maintains its own task ID's to start off with.
The next steps I plan to take are to try and absorb the task scheduler into my design allowing a far more efficient approach ( hopefully ). There is also the possibility for an AVR based scheduler using a dedicated timer interrupt as a system management thread ( if the current task scheduler functionality isn't replicable on AVR ).

I will be looking at these things next, at the moment I'm testing some updates and optimisations, which I'll post shortly.

fat16lib:
Now Arduino is doing the same drill.

Why?

Seriously, you have to ask? Really?
Just take a look at what so many of the younger generation use as their "model" of an "os"
or s/w development environment model: Windows

Take a look at how that developed over the past few decades and the style of development used.
You have folks with little to no real computer science skills hacking away.
And using a "solve the problem of the day" approach rather than a planned & designed approach.

So many things are done haphazardly and problems/issue are often solved at the wrong level
because there is no understanding of the overall system, often because there is no overall
system design.

To me, all this Arduino stuff is eerily similar to much of nonsense I see in the Windows world.

--- bill

pyro,
I'm curious where you headed with this mutex stuff.
In my mind mutexes are not simple exclusions of everything as would be implied with the
simple ATOMIC BLOCK stuff as they are in the AVR world.

When I use mutexes in real operating systems, I'm protecting particular resources and I don't
want to block out other threads particularly when those threads might be on other processors.
Obviously muti-processor is a more complex environment but threads can be very relevant.

For example, when a processor supports interrupts levels it is VERY useful to allow interrupts to nest
and interrupt each other because often the ISRs don't deal with the same resources.

So for me, I consider it very important to be able to block certain interrupts but not all
of them. I like the simple splx() type of prioritization that controls priority levels.
Where something a splhi() can be use to block everything including the scheduler.
but other levels could be used to out other less important things.

How is this kind of functionality going to work with ATOMIC BLOCK and mutexes?

Just a few quick thoughts.

---bill

@bperrybap, thanks for the post.

I'm protecting particular resources and I don't want to block out other threads
...
For example, when a processor supports interrupts levels it is VERY useful to allow interrupts to nest
and interrupt each other because often the ISRs don't deal with the same resources.

If you are worried about the atomicity blocking ISR functions, not the actual mutex lock, then this is not the case. I have built this with interrupt routines in mind; the atomic blocking is only utilised during an unlock / lock operation or when reading the owner of a mutex/lock.

The atomic functionality is actually only there for ISR interaction. I'll explain below.

[Note] The API has slightly changed from what is above, m_Restore, m_Force, m_None are now the AtomicBlock definitions: Atomic_RestoreState, Atomic_Force, and Atomic_None.

Atomic_None is a recent addition to the AtomicBlock library which prevents it from emitting any blocking code. So a library, like the mutex, can be programmed around atomicity and have it disabled when not needed.

If you have data shared between only task scheduler 'tasks' ( not touched by ISR ), then you can safely use:

HMUTEX h_MyMutex = createMutex( Atomic_None );

An ISR+ISR or ISR+Task interaction would require a mutex with blocking capabilities.

ISR+Task is interesting as only the task would require blocking capabilities, the ISR has interrupts disabled by default ( I know for AVR anyway, ARM interrupts may operate differently. )

//Create a mutex with blocking capabilities.
HMUTEX h_TaskHandle = createMutex( Atomic_Force );

//ISR needs no atomic guarantee.
HMUTEX h_ISRHandle = duplicateHandle( h_TaskHandle, Atomic_None );

void loop(){
  if( acquireMutex( h_TaskHandle ) ){
    releaseMutex( h_TaskHandle );
  }
}

void ISR(){
  if( acquireMutex( h_ISRHandle ) ){
    releaseMutex( h_ISRHandle );
  }
}

This example is incomplete. It is to show mutex creation.

Here both the ISR and loop() funciton use the same mutex, just with different atomic contracts.

If an ISR is needed to be blocked entirely, the AtomicBlock library ( link in my sig ) is perfect for that.

I'll post the updated version ASAP, just doing a quick test.