Semaphores and Mutexes

[中文]

Semaphores are mechanisms used in multitasking operating systems to implement task synchronization and mutual exclusion. They control task access to shared resources, avoiding resource conflicts and ensuring tasks are executed in the expected order.

Similar to queues, semaphores are also used for synchronization and communication between tasks. However, semaphores mainly transmit resource status or event notifications, controlling access permissions, while queues are used to transmit specific data, implementing data exchange between tasks.

This document only covers some commonly used APIs in semaphores. For more information, please refer to the FreeRTOS official documentation.

When calling queue-related APIs in ESP-IDF, you need to include the following header file in the file:

#include "freertos/semphr.h"

Creating Binary Semaphores

API Prototype

SemaphoreHandle_t xSemaphoreCreateBinary( void );

xSemaphoreCreateBinary() is used to create a binary semaphore. The initial state is empty (unavailable), suitable for inter-task event notification or simple synchronization control.

Return Value Explanation

Return Value

Explanation

NULL

Semaphore creation failed due to insufficient FreeRTOS available heap space.

Semaphore Handle

Creation successful, the returned handle can be used for subsequent operations on this semaphore.

Example 1: Creating a Binary Semaphore

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

// Define handle to receive return value
SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
    // Create semaphore
    xHandle = xSemaphoreCreateBinary();

    if (xHandle != NULL) {
        printf("Create SUCCESS\n");
    }
    else {
        printf("Create FALSE\n");
    }
}

The execution result of the above code is as follows:

Create SUCCESS

This example demonstrates how to use xSemaphoreCreateBinary() to create a binary semaphore: define a handle to receive the return value, and judge whether the creation is successful based on the return value after creating the semaphore.

Creating Counting Semaphores

API Prototype

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
                                            UBaseType_t uxInitialCount);

xSemaphoreCreateCounting() is used to create a counting semaphore, supporting the specification of the maximum count value and initial count value, suitable for managing a limited number of resources or controlling the synchronization of multiple events.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

uxMaxCount

Maximum count value

When the semaphore count reaches this value, the “give” operation (xSemaphoreGive) cannot be continued.

uxInitialCount

Initial count value

The starting count value set at creation, cannot be greater than uxMaxCount; if it is 0, the semaphore is initially empty.

Note

The current count value of the counting semaphore reflects the number of available resources, decreasing each time it is acquired, and increasing when released. The maximum count value represents the maximum number of resources the semaphore can have, and the initial count value represents the number of resources at creation.

Return Value Description

Return Value

Description

NULL

Unable to create semaphore due to inability to allocate RAM required for reserved semaphore, semaphore creation failed.

Semaphore Handle

Creation successful, the returned handle can be used for subsequent operations on this semaphore.

Example 2: Creating a Counting Semaphore

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

// Define handle to receive return value
SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
    // Create semaphore
    xHandle = xSemaphoreCreateCounting(10, 0);

    if (xHandle != NULL) {
        printf("Create SUCCESS\n");
    }
    else {
        printf("Create FALSE\n");
    }
}

The execution result of the above code is as follows:

Create SUCCESS

This example demonstrates how to use xSemaphoreCreateCounting() to create a counting semaphore, where the maximum count value is 10 and the initial count value is 0. The program first defines the semaphore handle and determines whether the semaphore is successfully created through the function return value.

Counting semaphores are suitable for event counting and resource management scenarios. In event counting, each time an event occurs, the interrupt or task calls xSemaphoreGive() to increase the count value, and the processing task waits and acquires the semaphore by calling xSemaphoreTake(), thereby triggering event processing. In this case, the initial value is usually set to 0. In resource management scenarios, the semaphore count represents the number of available resources, the task decreases the count when acquiring resources, and increases the count when releasing resources. The initial value is usually equal to the maximum value, indicating that all resources are initially available.

Compared with binary semaphores that can only represent “yes” or “no” states, counting semaphores can represent multiple units, making them more suitable for handling cumulative events or managing multiple resources. Binary semaphores have a simple structure and small resource overhead, suitable for basic synchronization and mutual exclusion; counting semaphores have stronger functions and are suitable for more complex synchronization needs. Which semaphore to use should be chosen according to the application scenario.

Creating a Mutex Semaphore

API Prototype

SemaphoreHandle_t xSemaphoreCreateMutex( void )

xSemaphoreCreateMutex() is used to create a mutex semaphore to implement mutual exclusion access to shared resources between tasks, ensuring that only one task accesses resources at the same time. The mutex is initially “available” after creation, i.e., it is not held by any task and can be immediately acquired by a task.

Return Value Description

Return Value

Description

NULL

Unable to create mutex due to inability to allocate memory required to save mutex, semaphore creation failed.

Semaphore Handle

Creation successful, the returned handle can be used for subsequent operations on this semaphore.

Since the creation of the mutex is similar to the binary semaphore, only the API needs to be replaced, so no example is provided here.

The structure of a mutex lock is similar to a binary semaphore, but the mutex lock supports a priority inheritance mechanism, which the binary semaphore does not possess. Therefore, binary semaphores are more suitable for synchronization between tasks or between tasks and interrupts, and are a better choice for implementing simple mutexes.

Note

The priority inheritance mechanism refers to when a high-priority task tries to acquire a mutex held by a low-priority task, the low-priority task is temporarily elevated to high priority to prevent priority inversion. It should be noted that the task holding the mutex must release it in time, otherwise the high-priority task will be blocked indefinitely, and the low-priority task cannot return to its original priority.

Creating Recursive Mutex Semaphore

API Prototype

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )

xSemaphoreCreateRecursiveMutex() is used to create a recursive mutex, which allows the same task to recursively acquire the lock multiple times without causing a deadlock, suitable for resource protection in nested call scenarios. The mutex is initially “available” after creation, i.e., it is not held by any task and can be immediately acquired by a task.

Return Value Description

Return Value

Description

NULL

Semaphore creation failed due to inability to allocate memory required for the mutex.

Semaphore Handle

Creation successful, the returned handle can be used for subsequent operations on this semaphore.

Since the creation of a recursive mutex is similar to a non-recursive mutex, only the API needs to be replaced, so no example is provided here.

A non-recursive mutex can only be acquired by the same task once. If the task tries to acquire it again without releasing it, it will fail or enter a blocking state. Only after release, the mutex becomes available again.

A recursive mutex allows the same task to repeatedly acquire it while holding the lock. Each acquisition increases the internal count. When releasing the lock, the task must call the same number of release operations. Only when the number of releases equals the number of acquisitions, the lock is truly released and becomes available again.

Both use the priority inheritance mechanism to prevent priority inversion problems.

Deleting Semaphore

API Prototype

void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );

vSemaphoreDelete() is used to delete a created semaphore, freeing up its system resources. After deletion, the semaphore handle becomes invalid and should not be used again.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xSemaphore

Handle of the semaphore to be deleted

A valid handle returned by the semaphore creation API, used to specify the semaphore to be deleted.

After the semaphore is deleted, the value of the semaphore handle itself does not automatically become NULL or any other special value, it still maintains its original value. But this handle is invalid and cannot be used for any semaphore operations, otherwise it will lead to undefined behavior.

Therefore, after calling vSemaphoreDelete(), it is recommended to manually set the handle to NULL to prevent misuse.

Acquiring Resources

API Prototype

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
                           TickType_t xTicksToWait );

xSemaphoreTake() is used to acquire a semaphore, indicating that the task is trying to access the protected resource. You can specify a wait time when calling. If the semaphore is available, it is obtained immediately and the count value is reduced; if it is not available, the task is blocked or returns failure immediately based on the wait time. This function is commonly used for task synchronization and resource mutual exclusion access.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xSemaphore

Handle of the semaphore being acquired

A valid handle returned by the semaphore creation API, used to specify the semaphore to be acquired.

xTicksToWait

Maximum waiting time

The maximum time to wait for the semaphore to become available, defined in tick cycles; returns immediately when set to 0.

Return Value Description

Return Value

Description

pdTRUE

Successfully acquired the semaphore.

pdFALSE

Semaphore acquisition failed due to timeout.

Note

A semaphore is available when it meets the acquisition conditions and can be successfully acquired without waiting. For different types of semaphores:

  • Binary semaphore: There are only two states (0 or 1). When the value is 1, it indicates availability, and the task can acquire it; when it is 0, it indicates unavailability, and the task needs to wait or fail.

  • Counting semaphore: When the current count is greater than 0, it indicates that there are available “resources” or “events” that can be acquired; when it is 0, it indicates temporary unavailability.

  • Mutex/Recursive Mutex: It is “available” when not held by other tasks and can be successfully acquired by the current task.

Example 3: Acquiring Counting Semaphore Resources

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

SemaphoreHandle_t xHandle = NULL;
void app_main(void)
{
    // Used to receive the return value of resource acquisition
    BaseType_t xReture;
    // Create semaphore
    xHandle = xSemaphoreCreateCounting(5, 3);

    int count = 0;

    if (xHandle != NULL) {
        printf("Create SUCCESS\n");
    }
    else {
        printf("Create FALSE\n");
    }

    while (1) {
        xReture = xSemaphoreTake(xHandle, 0);
        if (xReture == pdTRUE) {
            count++;
            printf("Take SUCCESS %d\n", count);
        }
        else {
            printf("Take FALSE\n");
        }
    }
}

The execution result of the above code is as follows:

Create SUCCESS
Take SUCCESS 1
Take SUCCESS 2
Take SUCCESS 3
Take FALSE
Take FALSE
Take FALSE

This example demonstrates how to use xSemaphoreTake() to acquire semaphore resources. The example creates a counting semaphore with a maximum count of 5 and an initial count of 3, indicating that the initial number of “resources” of the semaphore is 3.

In an infinite loop, the task calls xSemaphoreTake() in a non-blocking manner (waiting time is 0) to try to acquire the semaphore. Since the initial count is 3, the first 3 calls can successfully acquire the semaphore. After each successful acquisition, the count decreases by 1, indicating that the resource is occupied and the number of resources decreases by 1.

When the semaphore count decreases to 0, it indicates that there are currently no available resources, and subsequent calls to xSemaphoreTake() will immediately return failure, and the task cannot continue to acquire resources.

Please note:

  • In this example, xSemaphoreGive() is not called, so the resources will not be released, and the semaphore count continues to decrease until it is exhausted. In actual applications, it should be ensured that the acquisition and release operations of the semaphore are performed in pairs to maintain the consistency of the semaphore status with the actual resource status, to avoid resource leakage or deadlock.

  • This example uses a non-blocking acquisition method, suitable for scenarios where semaphore status is checked by polling. When waiting for the semaphore to be available, the wait time parameter of xSemaphoreTake() can be set to a non-zero value, causing the task to block and wait when the semaphore is not available, thereby improving system efficiency.

Releasing resources

API prototype

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );

xSemaphoreGive() is used to release a semaphore, indicating that a task or interrupt has finished using a resource. For binary semaphores and counting semaphores, calling this function will increase the count of the semaphore, indicating that the number of available resources has increased; for non-recursive mutexes, this API is used to release the lock, making it available again.

Parameter explanation

Input parameter

Parameter function

Parameter description

xSemaphore

The handle of the semaphore being released

A valid handle returned by the semaphore creation API, used to specify the semaphore to be released.

Return value description

Return value

Description

pdTRUE

For counting semaphores and binary semaphores, it indicates that the count has been successfully incremented; for mutexes, it indicates that the lock has been successfully released.

pdFALSE

Release failed. This may be due to the semaphore not being correctly acquired, the semaphore being full, or the release operation being illegal, etc.

Example 4: Releasing binary and counting semaphore resources

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

SemaphoreHandle_t xHandleBinary = NULL;
SemaphoreHandle_t xHandleCounting = NULL;

static void vTakeTask( void *pvParameters )
{
    const char *NameTakeTask = pcTaskGetName(NULL);
    SemaphoreHandle_t xHandle = (SemaphoreHandle_t)pvParameters;
    // Used to receive the return value of resource acquisition
    BaseType_t xRetureForCount;
    BaseType_t xRetureForTake;

    int count = 0;
    while (1) {
        xRetureForTake = xSemaphoreTake(xHandle, 0);

        if (xRetureForTake == pdTRUE) {
            count++;
            xRetureForCount = uxSemaphoreGetCount(xHandle);
            printf("%d: Count after %s is %d\n", count, NameTakeTask, xRetureForCount);
        }
        else {
            printf("%s FALSE\n", NameTakeTask);
            printf("%s Task DELETED\n", NameTakeTask);
            vTaskDelete(NULL);
        }

        if (count == 3) {
            printf("%s Task DELETED\n", NameTakeTask);
            vTaskDelete(NULL);
        }
        vTaskDelay(1);
    }
}

static void vGiveTask( void *pvParameters )
{
    const char *NameGiveTask = pcTaskGetName(NULL);
    SemaphoreHandle_t xHandle = (SemaphoreHandle_t)pvParameters;
    // Used to receive the return value of semaphore creation
    BaseType_t xRetureForCount;
    BaseType_t xRetureForGive;

    int count = 1;

    xRetureForCount = uxSemaphoreGetCount(xHandle);
    printf("Initial Count for %s is %d\n", NameGiveTask, xRetureForCount);

    while (1) {
        xRetureForGive = xSemaphoreGive(xHandle);

        if (xRetureForGive == pdTRUE) {
            xRetureForCount = uxSemaphoreGetCount(xHandle);
            printf("%d: Count after %s is %d\n", count, NameGiveTask, xRetureForCount);
            count++;
        }

        if (count == 3) {
            printf("%s Task DELETED\n", NameGiveTask);
            vTaskDelete(NULL);
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // Create semaphore
    xHandleBinary = xSemaphoreCreateBinary();
    xHandleCounting = xSemaphoreCreateCounting(5, 3);
    xHandleMutex = xSemaphoreCreateMutex();

    if (xHandleBinary != NULL) {
        xTaskCreate(vGiveTask, "Binary Give", 2048, (void *)xHandleBinary, 1, NULL);
        xTaskCreate(vTakeTask, "Binary Take", 2048, (void *)xHandleBinary, 1, NULL);
    }

    if (xHandleCounting != NULL) {
        xTaskCreate(vGiveTask, "Counting Give", 2048, (void *)xHandleCounting, 1, NULL);
        xTaskCreate(vTakeTask, "Counting Take", 2048, (void *)xHandleCounting, 1, NULL);
    }
}


void app_main(void)
{
    // Create semaphore
    xHandleBinary = xSemaphoreCreateBinary();
    xHandleCounting = xSemaphoreCreateCounting(5, 3);

    if (xHandleBinary != NULL) {
        xTaskCreate(vGiveTask, "Binary Give", 2048, (void *)xHandleBinary, 1, NULL);
        xTaskCreate(vTakeTask, "Binary Take", 2048, (void *)xHandleBinary, 1, NULL);
    }

    if (xHandleCounting != NULL) {
        xTaskCreate(vGiveTask, "Counting Give", 2048, (void *)xHandleCounting, 1, NULL);
        xTaskCreate(vTakeTask, "Counting Take", 2048, (void *)xHandleCounting, 1, NULL);
    }
}

The execution result of the above code is as follows:

Initial Count for Binary Give is 0
1: Count after Binary Give is 1
1: Count after Binary Take is 0
Initial Count for Counting Give is 3
1: Count after Counting Give is 4
2: Count after Binary Give is 1
Binary Give Task DELETED
2: Count after Binary Take is 0
1: Count after Counting Take is 3
2: Count after Counting Give is 4
Counting Give Task DELETED
2: Count after Counting Take is 3
Binary Take FALSE
Binary Take Task DELETED
3: Count after Counting Take is 2
Counting Take Task DELETED

After sorting, we can get:

Initial Count for Binary Give is 0
1: Count after Binary Give is 1
1: Count after Binary Take is 0
2: Count after Binary Give is 1
Binary Give Task DELETED
2: Count after Binary Take is 0
Binary Take FALSE
Binary Take Task DELETED

Initial Count for Counting Give is 3
1: Count after Counting Give is 4
1: Count after Counting Take is 3
2: Count after Counting Give is 4
Counting Give Task DELETED
2: Count after Counting Take is 3
3: Count after Counting Take is 2
Counting Take Task DELETED

This example creates a binary semaphore and a counting semaphore, which are given to paired tasks for resource release and acquisition, respectively. It teaches the usage of xSemaphoreGive() and xSemaphoreTake(), and observes the behavioral differences of different types of semaphores when releasing resources. In the example, after each semaphore is successfully created, two tasks will be created to release and acquire resources. After each successful resource release or acquisition, the task will print the current count of the semaphore and record the execution round through the increment counter for easy tracking of the dynamic changes of the semaphore status. For the binary semaphore, the execution result shows that its initial count is 0. After one release, the count becomes 1; after the acquisition task successfully acquires the resource, the count falls back to 0.

In the example, after successfully creating each type of semaphore, two tasks are created: one for releasing the resource and another for acquiring it. Each time a resource is successfully released or acquired, the task prints the current semaphore count and increments an execution counter to help track the dynamic changes in the semaphore’s state.

For the binary semaphore, the execution result shows its initial count is 0. After one release operation, the count increases to 1; when the acquiring task successfully takes the semaphore, the count drops back to 0. Since the releasing task performs only two release operations and then exits without a third release, the acquiring task fails to acquire the semaphore on the third attempt.

Note

This result is affected by the task scheduling order and does not have absolute universality.

It can be seen from this that the release and acquisition of binary semaphores must be strictly paired. Only after release can it be successfully acquired, and the release will not accumulate.

Note

In this example, the value of the binary semaphore always changes between 0 and 1, not because the program frequently acquires and releases, but because the binary semaphore itself only has two states: 0 means not released (unavailable), 1 means released (available). Even if the release function is called continuously, its count value will not exceed 1. Therefore, release and acquisition must be strictly paired, and the semaphore will not accumulate.

For counting semaphores, the execution result shows that its initial count value is 3. After the release task runs once, the count value becomes 4; then after the acquisition task acquires a resource once, the count value changes back to 3. The release task only released at most twice and then exited without performing the third release. But because the initial value is 3, even if the release operation does not continue, the acquisition task can still successfully acquire resources up to three more times.

This shows that the number of available resources of the counting semaphore is not only determined by the release operation, but also affected by the initial value. The total number of acquisitions should not exceed the sum of the initial value and the number of releases.

Example 4: Release non-incremental mutex semaphore resources

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

SemaphoreHandle_t xHandleMutex = NULL;
// Used to receive the return value of resource acquisition
BaseType_t xRetureForTake;
BaseType_t xRetureForGive;
static int count = 1;

static void vTakeTask ( void )
{
    xRetureForTake = xSemaphoreTake(xHandleMutex, 0);

    if (xRetureForTake == pdTRUE) {
        printf("%d Take SUCCESS\n", count);
    }
    else {
        printf("Take FALSE\n\n");
    }
}

static void vGiveTask ( void )
{
    xRetureForGive = xSemaphoreGive(xHandleMutex);

    if (xRetureForGive == pdTRUE) {
        printf("%d Give SUCCESS\n", count);
    }
    else {
        printf("Give FALSE\n\n");
    }
}

static void vMutexTask( void *pvParameters )
{
    printf(">>>>> Give after initialize\n");
    vGiveTask();

    printf(">>>>> General Case\n");
    while (1) {
        if (count < 3) {
            vTakeTask();
            vGiveTask();
            count++;
        }

        if (count == 3) {
            printf("\n>>>>> Take twice after one give\n");

            vTakeTask();
            vTakeTask();
            printf("Task DELETED\n");
            vTaskDelete(NULL);
        }
        vTaskDelay(1);
    }
}

void app_main(void)
{
    // Create semaphore
    xHandleMutex = xSemaphoreCreateMutex();

    if (xHandleMutex != NULL) {
        xTaskCreate(vMutexTask, "Mutex Take", 2048, NULL, 1, NULL);
    }
}

The execution result of the above code is as follows:

>>>>> Give after initialize
Give FALSE

>>>>> General Case
1 Take SUCCESS
1 Give SUCCESS
2 Take SUCCESS
2 Give SUCCESS

>>>>> Take twice after one give
3 Take SUCCESS
Take FALSE

Task DELETED

This example demonstrates how to acquire and release non-recursive mutex semaphores through xSemaphoreTake() and xSemaphoreGive(), and verifies their usage restrictions through execution results. The example creates a task that deletes itself after three rounds of semaphore operations.

The task tests three scenarios in sequence:

  1. Immediately release after semaphore creation

  2. Paired use of acquisition and release

  3. Multiple acquisitions of the same semaphore

From the execution results, the following points can be summarized:

  1. Immediate release after mutex semaphore creation will fail because the semaphore has not been held by any task at this time and can only be released by the actual holder.

  2. Mutex semaphores must be strictly paired in acquisition and release, and both acquisition and release must be completed in the same task.

  3. Non-recursive mutex semaphores do not support multiple acquisitions by the same task, and further acquisition before release will fail.

Acquisition and Release of Recursive Mutex Semaphore

API Prototype

// Acquire resource
xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex,
                       TickType_t xTicksToWait );

// Release resource
xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex );
Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xSemapxMutexhore

Handle of the semaphore being acquired/released

A valid handle returned by xSemaphoreCreateRecursiveMutex(), used to specify the semaphore to be acquired/released.

xTicksToWait

Maximum waiting time

The maximum time to wait for the semaphore to be available, defined in tick cycles; returns immediately when it is 0.

Return Value Description

Return Value

Description

pdTRUE

Successfully acquired the semaphore.

pdFALSE

Semaphore wait timeout, acquisition failed.

Recursive mutexes use xSemaphoreTakeRecursive() and xSemaphoreGiveRecursive() for acquisition and release. This type of semaphore records the number of acquisitions by the task, and the semaphore is truly released only when the number of releases matches the number of acquisitions, which is suitable for function nesting or repeated entry into critical areas.

Its usage is the same as xSemaphoreTake() and xSemaphoreGive(), so no example is provided here. The difference is that recursive mutexes allow the same task to acquire multiple times, while ordinary mutexes do not support multiple acquisitions.

Semaphore Counting

API Prototype

UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );

uxSemaphoreGetCount() is used to get the current count of the semaphore, indicating the number of available resources in the semaphore. This API is mainly used for debugging and status monitoring, and can only be applied to counting semaphores and binary semaphores, not mutex semaphores.

Parameter Explanation

Input Parameter

Parameter Function

Parameter Description

xSemaphore

Handle of the semaphore to be counted

A valid handle returned by the semaphore creation API, used to specify the semaphore to be counted.

Return Value Description

Return Value

Description

Current count of the semaphore

When the semaphore is a counting semaphore.

1

When the semaphore is a binary semaphore, it indicates that the semaphore is available.

0

When the semaphore is a binary semaphore, it indicates that the semaphore is not available.

For the usage of this API, please refer to Example 4.