In computer science, a lock or locking is the act of blocking access to equipment. Such a lock allows a process to have exclusive access to a resource, i.e. with the guarantee that no other process will read or modify that resource as long as the lock is in place. Locking is often used in process synchronization as well as in file and database systems to ensure atomic and consistent read and write requirements.
In the realm of microcontroller programming, efficient resource management is paramount to ensure optimal performance, responsiveness, and reliability in embedded systems. Reader-writer locks, a synchronization mechanism commonly employed in multi-threaded environments, play a crucial role in managing access to shared resources and promoting concurrency while preserving data integrity.
Reader/writer locks enable efficient concurrency management, allowing multiple tasks or interrupts to access shared resources concurrently for reading while ensuring data consistency and preventing conflicts during write operations. This promotes responsiveness and efficiency in embedded systems, particularly in applications with high throughput requirements. By distinguishing between read and write operations, reader-writer locks minimize blocking and latency in microcontroller applications, as threads or tasks can access the resource concurrently for reading without waiting for exclusive access. This leads to smoother operation and better overall system performance, especially in time-critical or real-time scenarios.
---

Types of Locking
If a process wants exclusive access to a resource, it must request a lock from an administrative process (such as a locking manager). To avoid locking the requested resource entirely, there are two basic types of locks:
Read-Lock
If a resource has a read lock, the process that set this lock only wants to read from the resource. This means that other processes can also access this resource in a read-only manner, but are not allowed to change it.
In microcontroller applications, read locking allows multiple threads or tasks to access a shared resource simultaneously for reading purposes. When a thread acquires a read lock, it signals its intention to read the resource without modifying its contents. Other threads can also acquire read locks concurrently, enabling concurrent read access.
Write-lock
A write-lock resource prevents the resource from being read or written by other processes, because the process that set the lock wants to modify the resource. The procedure is also known as pessimistic locking, as it is based on the assumption that the data will usually be updated. Optimistic locking assumes that there is usually no update or that two users are not likely to update at the same time. It is only checked when updating whether the status quo still applies.
Write locking grants exclusive access to the shared resource, preventing any other threads or tasks from accessing it (both for reading and writing) until the write operation completes. Write locks are mutually exclusive and are typically acquired when a thread needs to modify the contents of the resource, ensuring data consistency and preventing race conditions.
Hierarchical locking is the process of combining resources into larger logical units. It is now possible to lock the entire logical unit. This brings a performance gain, as it means that all elements of the unit do not need to be locked and monitored separately. The right choice of the granularity of the logical units is of great importance.
Hierarchical locking is mainly used in database systems. For example, a data field, a data record, a file, or the entire database can be locked. The best practice in each case depends on the type of database, the frequency of changes, and the number of concurrent users.
Multi-locking is part of pessimistic locking and enables deadlock-free programming. With the MultiLock, the locks are reserved right at the beginning of the synchronization block and form a MultiLock. With MultiLocks, no deadlocks can occur because a MultiLock is only started when all required locks are available. No new locks can be used while MultiLock is running. However, the individual locks belonging to the MultiLock can be released and used in other MultiLocks.
Lock Release
Once the process that requested a block is finished, it must remove the block. Processes that were unable to access a resource due to a lock must wait and join a queue. There are several ways this queue is designed, such as priority-driven, FIFO-driven, etc.
Starving
If a process does not release a lock, other processes may wait indefinitely for this release. These processes thus “starve”.
Deadlock
Setting a lock can cause deadlocks, namely when two processes are waiting for each other to release the resource locked by the other.
Example: There is (as resources) a bicycle and a city map. Two bicycle couriers are each to deliver a package and (each) need a bicycle and city map. Courier A can reserve the bike, courier B the city map. They are now deadlocked, as both are waiting for each other’s resource (endless).
Implementation Strategies
Implementing read-write locks in microcontroller applications requires careful consideration of resource constraints, real-time requirements, and the specific features of the microcontroller architecture. Common implementation strategies include:
Mutex-Based Implementation: Using a binary semaphore or mutex to protect access to the shared resource, with additional logic to track the number of readers and enforce shared-exclusive access patterns. Mutex-based implementations are straightforward and suitable for applications with modest concurrency requirements.
Spinlock-Based Implementation: Utilizing spinlocks or atomic operations to implement lightweight read-write locks in situations where preemptive multitasking is not available or practical. Spinlock-based implementations minimize context switching overhead and are well-suited for bare-metal or real-time operating system (RTOS) environments.
Real-World Applications and Use Cases
- Concurrent access to sensor data buffers for reading by multiple tasks or interrupts while ensuring exclusive access during data updates.
- Managing access to communication buffers (e.g., UART, SPI, I2C) for reading and writing by multiple communication tasks or protocols.
- Coordinating access to status flags, configuration variables, and control parameters used by various subsystems or modules in the system.
Code Example for ESP32 for ESP-IDE
Below is a simple example of implementing a reader-writer lock in C language for the ESP32 using ESP-IDF (Espressif IoT Development Framework). This example demonstrates how to use a mutex and a semaphore to implement a basic reader-writer lock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | #include <stdio.h> #include <freertos/FreeRTOS.h> #include <freertos/task.h> #include <freertos/semphr.h> // Define a mutex for exclusive write access static SemaphoreHandle_t mutex; // Define a semaphore for controlling read access static SemaphoreHandle_t readerSemaphore; // Counter to track the number of readers static int readerCount = 0; // Writer task void writerTask(void *pvParameters) { while (1) { // Acquire the mutex for exclusive write access xSemaphoreTake(mutex, portMAX_DELAY); // Perform write operation printf("Writer writing data...\n"); vTaskDelay(pdMS_TO_TICKS(1000)); // Simulate write operation // Release the mutex xSemaphoreGive(mutex); // Delay before next write operation vTaskDelay(pdMS_TO_TICKS(2000)); } } // Reader task void readerTask(void *pvParameters) { while (1) { // Acquire the reader semaphore xSemaphoreTake(readerSemaphore, portMAX_DELAY); // Increment reader count readerCount++; // If it's the first reader, acquire the mutex for exclusive write access if (readerCount == 1) { xSemaphoreTake(mutex, portMAX_DELAY); } // Release the reader semaphore xSemaphoreGive(readerSemaphore); // Perform read operation printf("Reader %d reading data...\n", (int)pvParameters); vTaskDelay(pdMS_TO_TICKS(500)); // Simulate read operation // Acquire the reader semaphore xSemaphoreTake(readerSemaphore, portMAX_DELAY); // Decrement reader count readerCount--; // If it's the last reader, release the mutex if (readerCount == 0) { xSemaphoreGive(mutex); } // Release the reader semaphore xSemaphoreGive(readerSemaphore); // Delay before next read operation vTaskDelay(pdMS_TO_TICKS(1000)); } } void app_main() { // Create mutex mutex = xSemaphoreCreateMutex(); // Create reader semaphore readerSemaphore = xSemaphoreCreateBinary(); // Start writer task xTaskCreate(writerTask, "Writer Task", 2048, NULL, 1, NULL); // Start reader tasks for (int i = 1; i <= 3; i++) { xTaskCreate(readerTask, "Reader Task", 2048, (void *)i, 1, NULL); } } |
We define a mutex (mutex) to provide exclusive write access and a semaphore (readerSemaphore) to control read access.
The writerTask simulates a write operation by acquiring the mutex, performing a write operation, and then releasing the mutex.
The readerTask simulates a read operation by acquiring the reader semaphore, incrementing the readerCount, acquiring the mutex (if it’s the first reader), performing a read operation, decrementing the readerCount, and releasing the mutex (if it’s the last reader).
This example demonstrates a basic implementation of a reader-writer lock using a mutex and a semaphore in ESP-IDF for the ESP32 microcontroller. It can serve as a starting point for building more complex reader-writer lock implementations tailored to specific application requirements.