RT Mutexes and Condition Variables

ChibiOS/RT implements Posix-inspired Mutexes and Condition Variables. Together the two constructs form an high level construct known as Monitor.

Global Settings

Mutexes and Condition Variables are affected by the following global settings:

CH_CFG_USE_MUTEXES This switch enables the mutexes API in the kernel.
CH_CFG_USE_MUTEXES_RECURSIVE If enabled then mutexes can be taken recursively.
CH_CFG_USE_CONDVARS This switch enables the condition variables API in the kernel.
CH_CFG_USE_CONDVARS_TIMEOUT Enables the timeout support for condition variables.

Mutexes

Mutexes are the mechanism meant to implement mutual exclusion in the most general way. There is often confusion between Mutexes and Semaphores, both are apparently able to solve the same problem but there are important differences:

  • Mutexes have an owner attribute, semaphores do not have owners. Because of this mutexes can only be unlocked by the same thread that locked them. This is not required for semaphores that can be unlocked by any thread or even ISRs.
  • Mutexes can implement protocols to handle Priority Inversion, knowing the owner is required in order to be able to implement Priority Inheritance or Priority Ceiling algorithms. ChibiOS/RT mutexes implement the Priority Inheritance algorithm with no restrictions on the number of threads or the number of nested mutual exclusion zones.
  • Mutexes can be implemented to be recursive mutexes, this means that the same thread can lock the same mutex repeatedly and then has to unlock it for the same number of times. In ChibiOS/RT mutexes can be made recursive by activating a configuration option.

State Diagram

Mutexes are regulated by the following state diagram:

Mutexes and Threads

There is a strict relationship between mutexes and threads:

Each mutex has a reference to the owner thread and a queue of waiting threads, on the other side, threads have a stack of owned mutexes. The field cnt counts how many times the owner has taken the mutex, this is present only if the recursive mode is enabled.

Mutexes API

mutex_t Type of a mutex object.
MUTEX_DECL() Mutexes static initializer.
chMtxObjectInit() Initializes a mutex object of type mutex_t.
chMtxLock() Locks the specified mutex.
chMtxLockS() Locks the specified mutex (S-Class variant).
chMtxTryLock() Tries to lock a mutex, if the mutex is already taken by another thread then the function exits without waiting.
chMtxTryLockS() Tries to lock a mutex, if the mutex is already taken by another thread then the function exits without waiting (S-Class variant).
chMtxUnlock() Unlocks the next owned mutex in reverse lock order.
chMtxUnlockS() Unlocks the next owned mutex in reverse lock order (S-Class variant).
chMtxUnlockAll() Unlocks all the mutexes owned by the invoking thread.

Notes

In all those situations where access to the shared resources can be postponed then it is recommended to use chMtxTryLock() instead of chMtxLock(), the former is much more efficient not having to enter the wait state.

The function chMtxUnlockAll() releases all mutexes owned by a thread and, for reasons related to the Priority Inheritance algorithm, releasing all mutexes is much faster than releasing just one. In those situations where a thread is known to own only one mutex the use of chMtxUnlockAll() can improve performance.

Condition Variables

Condition variables are an additional construct working with mutexes in order to form Monitor constructs much similar, in behaviour, to the Java synchronized constructs.

A condition variable is basically a queue of threads, the function chCondWait() performs atomically the following steps;

  1. Releases the last acquired mutex.
  2. Puts the current thread in the condition variable queue.

When the queued thread is kicked out of the queue using chCondSignal() or chCondBroadcast() then it:

  1. Re-acquires the mutex previously released.
  2. Returns from chCondWait().

State Diagram

Condition variables and mutexes, together, are regulated by the following state diagrams:

 Condition Variables and Mutexes State Diagram

Note how that the chCondWait() function implicitly operates on the last taken mutexes.

Condition Variables API

condition_variable_t Type of a condition variable object.
CONDVAR_DECL() Condition variables static initializer.
chCondObjectInit() Initializes a condition variable object of type condition_variable_t.
chCondSignal() Signals a condition variable.
chCondSignalI() Signals a condition variable (I-Class variant).
chCondBroadcast() Broadcasts a condition variable.
chCondBroadcastI()
chCondWait() Makes the invoking thread release the latest owned mutex and enter the condition variable wait queue.
chCondWaitS() Makes the invoking thread release the latest owned mutex and enter the condition variable wait queue (S-Class variant).
chCondWaitTimeout() Makes the invoking thread release the latest owned mutex and enter the condition variable wait queue with a timeout specification.
chCondWaitTimeoutS() Makes the invoking thread release the latest owned mutex and enter the condition variable wait queue with a timeout specification (S-Class variant).

Notes

Condition variables cannot be used alone, the only use case is in conjuction with mutexes in order to form monitors.

Monitors

A less formal, but probably easier to understand, way to explain how a monitor works is imagining it as a house with three rooms.

Monitor

The house has:

  • A main door bringing to the atrium.
  • An atrium, the mutex queue.
  • A door bringing into the main room, when the mutex is acquired.
  • A main room where only a person can stay at any time, the mutual exclusion zone.
  • An exit door, when the mutex is released.
  • A door bringing into a waiting room, waiting on the condition variable.
  • A waiting room for persons that have to wait for an external event, a call maybe, without occupy the main room, the condition variable queue.
  • A door bringing from the waiting room back into the atrium, when the external event occurred.
  • A emergency door allowing to escape the waiting room, timeouts on condition variables queues.

Threads enter the atrium and wait there their turn to enter the main room. When in the main room threads can either decide to leave the building or start waiting for an external signal without keeping the main room occupied. Note that monitors could have more than one conditional variable queues, in that case there would be multiple corridors bringing back to the atrium.

Monitor Code Template

Monitors should always written following this template:

void synchronized(void) {
 
  /* Entering the monitor.*/
  chMtxLock(&mtx);
 
  /* Protected code before the condition check, optional.*/
  ...;
 
  /* Checking the condition and keep waiting until it is satisfied, the
     part related to the timeout could be omitted by using chCondWait()
     instead.*/
  while (!condition) {
    msg_t msg = chCondWaitTimeout(&cond1, TIME_MS2ST(500));
    if (msg == MSG_TIMEOUT)
      return;
  }
 
  /* Protected code with condition satisfied.*/
  ...;
 
  /* Leaving the monitor.*/
  chMtxUnlock();
}

Condition variables objects are simply thread queues, the queue is called a “condition variable” because entering the queue is usually done after checking a condition that is not part of the object itself. The above “while” construct it its entirety is the condition variable.

Examples

Producers and Consumers

In this example we have a producer synchronized function and a consumer synchronized function both operating on a circular queue of messages.

#define QUEUE_SIZE 128
 
static msg_t queue[QUEUE_SIZE], *rdp, *wrp;
static size_t qsize;
static mutex_t qmtx;
static condition_variable_t qempty;
static condition_variable_t qfull;
 
/*
 * Synchronized queue initialization.
 */
void qInit(void) {
 
  chMtxObjectInit(&qmtx);
  chCondObjectInit(&qempty);
  chCondObjectInit(&qfull);
 
  rdp = wrp = &queue[0];
  qsize = 0;
}
 
/*
 * Writes a message into the queue, if the queue is full waits
 * for a free slot.
 */
void qProduce(msg_t msg) {
 
  /* Entering monitor.*/
  chMtxLock(&qmtx);
 
  /* Waiting for space in the queue.*/
  while (qsize >= QUEUE_SIZE)
    chCondWait(&qfull);
 
  /* Writing the message in the queue.*/  
  *wr = msg;
  if (++wr >= &queue[QUEUE_SIZE])
    wr = &queue[0];
  qsize++;
 
  /* Signaling that there is at least a message.*/
  chCondSignal(&qempty);
 
  /* Leaving monitor.*/
  chMtxUnlock(&qmtx);
}
 
/*
 * Reads a message from the queue, if the queue is empty waits
 * for a message.
 */
msg_t qConsume(void) {
  msg_t msg;
 
  /* Entering monitor.*/
  chMtxLock(&qmtx);
 
  /* Waiting for messages in the queue.*/
  while (qsize == 0)
    chCondWait(&qempty);
 
  /* Reading the message from the queue.*/  
  msg = *rd
  if (++rd >= &queue[QUEUE_SIZE])
    rd = &queue[0];
  qsize--;
 
  /* Signaling that there is at least one free slot.*/
  chCondSignal(&qfull);
 
  /* Leaving monitor.*/
  chMtxUnlock(&qmtx);
 
  return msg;
}

Producer from ISRs

It is the same problem of the previous example but messages are assumed to be originated from ISR contex. We will mix a monitor with a critical section in order to maintain atomicity. Note that the producer is not able to wait in this case, excess messages are lost.

#define QUEUE_SIZE 128
 
static msg_t queue[QUEUE_SIZE], *rdp, *wrp;
static size_t qsize;
static mutex_t qmtx;
static condition_variable_t qempty;
 
/*
 * Synchronized queue initialization.
 */
void qInit(void) {
 
  chMtxObjectInit(&qmtx);
  chCondObjectInit(&qempty);
 
  rdp = wrp = &queue[0];
  qsize = 0;
}
 
/*
 * Writes a message into the queue, if the queue is full waits
 * for a free slot. Note that I-Class functions are used from within
 * the critical zone.
 */
void qProduceFromISR(msg_t msg) {
 
  /* Entering monitor.*/
  chSysLockFromISR();
 
  /* Checking for space in the queue.*/
  if (qsize < QUEUE_SIZE) {
 
    /* Writing the message in the queue.*/  
    *wr = msg;
    if (++wr >= &queue[QUEUE_SIZE];
      wr = &queue[0];
    qsize++;
 
    /* Signaling that there is at least a message.*/
    chCondSignalI(&qempty);
  }
 
  /* Leaving monitor.*/
  chSysUnlockFromISR();
}
 
/*
 * Reads a message from the queue, if the queue is empty waits
 * for a message. Note that S-Class functions are used from within
 * the critical zone.
 */
msg_t qConsume(void) {
  msg_t msg;
 
  /* Entering monitor, using both the mutex and a critical zone.*/
  chSysLock();
  chMtxLockS(&qmtx);
 
  /* Waiting for messages in the queue.*/
  while (qsize == 0)
    chCondWaitS(&qempty);
 
  /* Reading the message from the queue.*/  
  msg = *rd
  if (++rd >= &queue[QUEUE_SIZE];
    rd = &queue[0];
  qsize--;
 
  /* Leaving monitor.*/
  chMtxUnlockS(&qmtx);
  chSysUnlock();
 
  return msg;
}