Written by Roy Jamil, Training Engineer at Ac6
Welcome to part 4 and the final part of our blog series on multithreading problems. In the previous articles ( part 1, part 2 and part 3), we explored the producer-consumer problem and the readers-writer(s) problem. If you haven’t had a chance to read them yet, we highly recommend checking them out.
In this part, we explore the readers-writer(s) problem, which occurs when multiple threads require concurrent access to a shared resource. We will delve into the complexities of this challenge and present effective strategies to address it. We will use the rwlock POSIX mechanism to solve this problem.
In this series of 4 blogs, we’ll discuss some of the most common problems that can arise when working with multithreaded systems, and we’ll provide practical solutions for solving these problems:
From the producer-consumer problem, where multiple threads share a common buffer for storing and retrieving data, to the readers-writer problem, where multiple reader threads need to access a shared resource concurrently.
Used kernel features
To ensure a solid foundation for understanding the Readers-writer solution, we will begin by presenting the pthread_rwlock_t mechanism.
A read-write lock (rwlock) is a synchronization mechanism available in Zephyr based on POSIX. It allows for concurrent access by multiple reader threads and exclusive access by a single writer thread. The rwlock maintains three states: read-locked, write-locked, and unlocked. When the rwlock is write-locked, all threads attempting to acquire it will be blocked until the writer completes its update. In contrast, when the rwlock is read-locked, multiple reader threads can simultaneously access the shared resource. This mechanism also ensures that writer threads are not starved by an excessive number of reader threads, providing fair access to both readers and writers and preventing writer starvation.
The rwlock internal implementation in Zephyr is based on semaphores. To learn more about semaphores and their role in synchronization, please check out Part 1 of this series. In that article, we discussed the producer-consumer problem and explored different solutions that involve semaphores.
First, let’s dive deeper into the implementation of the rwlock mechanism. Conceptually, it follows the same principles as the mutex and semaphore approach we discussed previously in the part 3.
The rwlock mechanism in Zephyr is built on top of three semaphores:
- “read” semaphore, it keeps track of the number of active readers currently accessing the resource. It is initialized to a maximum value, decrements the count when a reader acquires the lock and increments it when a reader releases the lock.
- “write” semaphore, which is a binary semaphore that indicates, when it is not available, that a writer might be either waiting for the already active readers to finish or currently writing. When a writer acquires this semaphore, it ensures exclusive access and blocking other writers from simultaneously modifying the resource.
- “reader_active” semaphore. It acts as a blocking mechanism for writers, allowing them to acquire the lock only when there are no active readers. It prevents a writer from acquiring the lock while readers are still accessing the resource.
Additionally, the rwlock mechanism maintains a reference to the owner which is the writer thread that successful acquired the “write” semaphore.
This main APIs in this mechanism are:
- pthread_rwlock_wrlock(): write lock API
- Firstly, it attempts to acquire the “write” semaphore. If successful, it proceeds to acquire the “reader_active” semaphore, blocking if there are active readers to prevent writer starvation.
- pthread_rwlock_rdlock(): read lock API
- Firstly, it attempts to acquire the “write” semaphore. If the semaphore is unavailable, it implies that a writer is currently writing or blocked on the “reader_active” semaphore. This ensures that readers do not proceed until the writer has finished to prevent race conditions or writer starvation.
- Once the “write” semaphore is acquired, it takes the “reader_active” semaphore and the “read” semaphore (to count active readers), and then releases the “write” semaphore.
- pthread_rwlock_unlock(): to unlock both write and read operations
- It checks if the unlocking thread is the owner, indicating it is the writer. In this case, it releases the write operation by releasing the “write” and “reader_active” semaphores.
- If the unlocking thread is not the owner, it indicates that it should unlock a read operation. It increments the “read” semaphore counter and only releases the “reader_active” semaphore when the last reader is reached (when the “read” count reaches its maximum value).
In order to use the rwlock in Zephyr, firstly you have to enable POSIX in configuration options: CONFIG_POSIX_API=y
The rwlock (read-write lock) is an easy-to-use mechanism that provides a solution to the readers-writers problem through a few simple APIs.
In this code snippet, the rwlock mechanism ensures that writers have exclusive access to the shared resource while they are performing writing operations, while multiple readers can concurrently access the resource for reading operations.
The writer thread acquires an exclusive write lock on the shared resource using “pthread_rwlock_wrlock()”. This ensures that only one writer can modify the shared resource at a time. The writer performs its writing operation on the shared resource, and then releases the write lock using “pthread_rwlock_unlock()”.
The reader thread acquires a read lock on the shared resource using “pthread_rwlock_rdlock()”. Multiple reader threads can acquire the read lock simultaneously, allowing concurrent access for reading operations. The reader performs its reading operation on the shared resource, and then releases the read lock using “pthread_rwlock_unlock()”.
Trace and debug:
Tracealyzer can be used to help in understanding the behaviour of the system by providing a visual representation of how the different threads are interacting with each other over time. It can help to identify and diagnose problems such as deadlocks, priority inversions, and other synchronization issues by showing the order of execution and the flow of control between the different threads and tasks.
In this trace you can see that when the writer started executing, it has blocked on the semaphore waiting for the readers because readers 1, 2 and 3 where active. When they finish reading, the last reader (Reader 2) unblocks the writer so it can write and not be starved.
In this article, we showed another solution for the readers-writer(s) problem using a dedicated easy-to-use mechanism based on the POSIX rwlock mechanism.
In conclusion, in this series of blogs, we have explored two key multithreading problems and their solutions. We began by addressing the producer-consumer problem, where we presented various synchronization techniques such as semaphores and message queues to ensure proper coordination between producers and consumers. Next, we delved into the readers-writer(s) problem, which arises when multiple threads need concurrent access to a shared resource. We examined different approaches to tackle this challenge, including the use of mutexes, semaphores, and the rwlock mechanism provided by POSIX in Zephyr.
If you’re interested in learning more about synchronization and communication mechanisms available in Zephyr, be sure to follow our full Zephyr training course. This course goes in-depth on the blocking and lock-free mechanisms available in Zephyr and covers a wide range of other multithreading problems and solutions.
Our course is designed for developers who want to learn how to write efficient and reliable multithreaded applications using the Zephyr RTOS. Whether you’re new to real-time systems or an experienced developer, this course will provide you with the knowledge and skills you need to develop high-performance multithreaded applications in Zephyr.For more information, click here to see the course outline.