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

Mutexes are regulated by the following state diagram:

Mutexes State Diagram

There is a strict relationship between mutexes and threads:

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.

API

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.

Examples

Mutexes have one single use, mutual exclusion.

Mutual Exclusion

static mutex_t mtx1;
 
static THD_WORKING_AREA(waThread1, 128);
static THD_FUNCTION(Thread1, arg) {
 
  while (true) {
 
    /* Normal thread activity.*/
    ...;
 
    /* Requesting access to protected resource.*/
    chMtxLock(&mtx1);
 
    /* Accessing shared resource.*/
    ...;
 
    /* Releasing the resource.*/
    chMtxUnlock(&mtx1);
 
    /* The thread continues.*/
    ...;
  }
}
 
static THD_WORKING_AREA(waThread2, 128);
static THD_FUNCTION(Thread2, arg) {
 
  while (true) {
 
    /* Normal thread activity.*/
    ...;
 
    /* Requesting access to protected resource.*/
    chMtxLock(&mtx1);
 
    /* Accessing shared resource.*/
    ...;
 
    /* Releasing the resource.*/
    chMtxUnlock(&mtx1);
 
    /* The thread continues.*/
    ...;
  }
}
 
/*
 * Initialization.
 */
void main(void) {
 
  /*
   * System initializations.
   * - Kernel initialization, the main() function becomes a thread and the
   *   RTOS is active.
   */
  chSysInit();
 
  /* Initializing the mutex sharing the resource.*/
  chMtxObjectInit(&mtx1);
 
  /* Starting threads.*/
  chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL);
  chThdCreateStatic(waThread2, sizeof(waThread2), NORMALPRIO + 1, Thread2, NULL);
 
  /* Continuing.*/
  ...;
}

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 and Monitors

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 a queue of threads temporarily releasing a mutex an waiting for an event, once the event is received then the thread automatically re-acquires the mutex, waiting for its turn if necessary.

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.

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.

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

API

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() Broadcasts a condition variable (I-Class variant).
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).

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 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;
}