Chapter 71: Pthreads: Read-Write Locks for Optimizing Read-Heavy Access
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental concepts of shared-exclusive locking models.
- Explain the difference between a mutex and a read-write lock and identify scenarios where each is appropriate.
- Implement thread-safe data access in a multi-threaded application using the Pthreads read-write lock API (
pthread_rwlock_t
). - Configure, compile, and run a C application on a Raspberry Pi 5 that uses read-write locks for efficient concurrency.
- Debug common issues associated with read-write locks, such as deadlocks and improper usage.
- Analyze the performance implications of using read-write locks in read-dominant applications.
Introduction
We have seen how mutexes provide a powerful mechanism for protecting shared resources from simultaneous access in our exploration of concurrent programming. By enforcing a strict, one-thread-at-a-time policy, mutexes prevent race conditions and ensure data integrity. However, this strict exclusivity comes at a performance cost. Consider a common scenario in embedded systems: a central configuration data structure. This data is read frequently by numerous threads—a networking thread checking connection parameters, a display thread rendering status information, a logging thread recording system state—but it is modified very infrequently, perhaps only when a user updates a setting through a web interface.
Using a standard mutex in this situation would create an unnecessary bottleneck. Every time a thread needs to read the configuration, it must lock the mutex, forcing all other threads, including other readers, to wait. This is inefficient. If multiple threads are only reading the data, there is no risk of corruption; they should be allowed to proceed concurrently. The only time we need to enforce exclusive access is when a thread needs to write to the data.
This is precisely the problem that read-write locks (rwlocks) are designed to solve. They provide a more nuanced locking mechanism that distinguishes between read access and write access. A read-write lock allows any number of threads to acquire a “read lock” simultaneously, but it ensures that only one thread can acquire a “write lock” at any given time, and only when no other threads hold a read lock. By relaxing the exclusivity for the common case (reading), we can significantly improve the throughput and responsiveness of our applications. In this chapter, we will delve into the theory and practical application of Pthreads read-write locks, using the Raspberry Pi 5 to build and test a real-world example of a high-performance, read-dominant system.
Technical Background
At the heart of concurrent system design lies the challenge of managing access to shared resources. The read-write lock is a sophisticated synchronization primitive that refines the simple, binary logic of a mutex (locked or unlocked) into a more granular, multi-state model. This model is formally known as a shared-exclusive lock, or sometimes a multiple-readers/single-writer lock. The fundamental principle is to increase concurrency by acknowledging that data integrity is only threatened by a writer, either modifying data while another thread writes or while another thread reads. The simultaneous actions of multiple readers pose no such threat.
A read-write lock, therefore, can exist in one of three states:
- Unlocked: No thread holds any lock. The resource is free to be acquired for either reading or writing.
- Read-Locked (Shared): One or more threads have acquired the lock for reading. In this state, other threads can also acquire a read lock, but any thread attempting to acquire a write lock will be blocked until all read locks are released.
- Write-Locked (Exclusive): Exactly one thread has acquired the lock for writing. In this state, any other thread attempting to acquire either a read lock or a write lock will be blocked until the writer releases the lock.
This mechanism ensures that a writer has exclusive access, preventing any other thread from reading or writing simultaneously, thus guaranteeing data consistency during modification.

The Pthreads Read-Write Lock API
The POSIX threads standard provides a complete and portable API for creating and managing read-write locks. The core of this API revolves around the pthread_rwlock_t
object, which holds the state of the lock.
Initialization and Destruction
Before a read-write lock can be used, it must be initialized. Just like a mutex, this can be done in two ways: statically or dynamically.
For a statically allocated pthread_rwlock_t
object (e.g., a global variable), you can use a convenient macro for initialization:
pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER;
This method is simple and sufficient for many use cases. However, for locks allocated on the heap or when custom attributes are needed, dynamic initialization is required using the pthread_rwlock_init()
function.
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
The first argument, rwlock
, is a pointer to the pthread_rwlock_t
object to be initialized. The second argument, attr
, is a pointer to a read-write lock attributes object. Passing NULL
for this argument instructs the system to initialize the lock with default attributes, which is the most common approach. The attributes object, pthread_rwlockattr_t
, can be used to control more subtle behaviors of the lock, such as its process-sharing scope or, on some systems, its preference policy regarding readers and writers.
When a read-write lock is no longer needed, it is crucial to release any associated system resources by calling pthread_rwlock_destroy()
:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
Attempting to destroy a locked read-write lock results in undefined behavior. Therefore, you must ensure that no thread holds the lock when this function is called. Failing to destroy a dynamically allocated lock before its memory is deallocated will result in a resource leak.
Acquiring and Releasing Locks
The core of the API consists of the functions used by threads to acquire and release read and write locks.
To acquire a read lock, a thread calls pthread_rwlock_rdlock()
:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
If the lock is currently unlocked or already in a read-locked state, the calling thread is granted the read lock immediately, and the function returns. If the lock is currently write-locked, the calling thread will block until the writer releases the lock. This is a blocking call, and the thread will be suspended by the scheduler, consuming no CPU cycles while it waits.
flowchart TD subgraph "Read Lock Acquisition: pthread_rwlock_rdlock()" A["Start: Thread calls<br><b>pthread_rwlock_rdlock()</b>"] style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff B{Is a write lock held?} style B fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff C[<b>BLOCK</b><br>Thread is suspended.<br>Waits for writer to unlock.] style C fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff D{Is writer preference active<br>AND a writer waiting?} style D fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff E[<b>BLOCK</b><br>Thread waits for<br>pending writer to finish.] style E fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 F[<b>SUCCESS</b><br>Acquire read lock.<br>Increment reader count.] style F fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff G[Thread enters<br>Read-Only Critical Section] style G fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff H[End: Lock Acquired] style H fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff A --> B B -- Yes --> C C --> B B -- No --> D D -- Yes (Implementation-dependent) --> E E --> D D -- No --> F F --> G G --> H end
To acquire a write lock, a thread calls pthread_rwlock_wrlock()
:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
This function attempts to gain exclusive access. If the lock is currently unlocked, the calling thread is granted the write lock immediately. If the lock is held by any other thread (for either reading or writing), the calling thread will block until all other locks are released.
flowchart TD subgraph "Write Lock Acquisition: pthread_rwlock_wrlock()" A["Start: Thread calls<br><b>pthread_rwlock_wrlock()</b>"] style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff B{"Is lock held by<br>ANY other thread<br>(reader or writer)?"} style B fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff C[<b>BLOCK</b><br>Thread is suspended.<br>Waits for ALL other<br>threads to unlock.] style C fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff D[<b>SUCCESS</b><br>Acquire exclusive write lock.] style D fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff E[Thread enters<br>Write-Only Critical Section] style E fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff F[End: Lock Acquired] style F fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff A --> B B -- Yes --> C C --> B B -- No --> D D --> E E --> F end
Once a thread has finished its critical section, it must release its lock by calling pthread_rwlock_unlock()
:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
This single function is used to release both read and write locks. The Pthreads implementation keeps track of the type of lock held by the thread and performs the correct release operation. When a reader unlocks, the internal count of active readers is decremented. If it was the last reader, a waiting writer might be woken up. When a writer unlocks, either a group of waiting readers or a single waiting writer can be woken up, depending on the implementation’s policy.
The API also provides non-blocking counterparts for acquiring locks: pthread_rwlock_tryrdlock()
and pthread_rwlock_trywrlock()
. These functions return immediately. If the lock can be acquired, they do so and return 0. If not, they return the error code EBUSY
without blocking the thread, allowing the program to take alternative action.
The Challenge of Fairness and Starvation
While the concept of a read-write lock is straightforward, its implementation hides some subtle complexities, primarily concerning fairness. Consider a system with a high, continuous stream of read requests. A writer thread may arrive and signal its intent to acquire a write lock. However, if new reader threads keep arriving and are granted access before the writer, the writer could be forced to wait indefinitely. This situation is known as writer starvation.
Conversely, some implementations might prioritize writers. When a writer is waiting, no new readers are granted access, even if the lock is currently in a read-locked state. This prevents writer starvation but can reduce concurrency, as it makes readers wait for a pending writer even when they could safely proceed. This is often called read-starvation, though it’s typically less of a practical problem.
graph TD subgraph "Fairness Policies & Starvation Scenarios" A[Start: High read traffic.<br>Lock is in READ state.] style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff B[Writer <b>W1</b> arrives<br>and requests write lock] style B fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff C{What is the system's<br>fairness policy?} style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff A --> B --> C subgraph "Scenario 1: Reader-Preference Policy" D[New Reader <b>R_new</b> arrives] style D fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff E[Policy grants lock to <b>R_new</b>,<br>ignoring the waiting writer.] style E fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff F[<b>W1</b> continues to wait.<br>More new readers arrive and are granted locks.] style F fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 G((<br><b>WRITER STARVATION</b><br>W1 may wait indefinitely<br>as new readers 'cut in line'.<br>)) style G fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff C -- "Reader-First" --> D D --> E E --> F F --> D F -.-> G end subgraph "Scenario 2: Writer-Preference Policy" I[New Reader <b>R_new</b> arrives] style I fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff J[Policy prioritizes waiting writer.<br><b>R_new</b> is blocked, even though<br>the lock is in a read state.] style J fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 K[Original readers finish.<br><b>W1</b> acquires the exclusive lock.] style K fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff L["<b>W1</b> finishes and unlocks.<br>Waiting readers (like <b>R_new</b>)<br>can now acquire the lock."] style L fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff M((<br><b>FAIRNESS ACHIEVED</b><br>Writer starvation is prevented.<br>Updates are guaranteed to proceed.<br>)) style M fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff C -- "Writer-First" --> I I --> J J --> K K --> L L --> M end end
The POSIX standard does not mandate a specific behavior, leaving it up to the implementation. Many modern Linux systems, including the one used by the Raspberry Pi, tend to implement writer-preference locks to prevent starvation and ensure that updates can eventually proceed. It is important to be aware of the potential for these scheduling policies to affect your application’s performance profile. If you have strict real-time requirements, you may need to investigate the specific behavior of your target system’s Pthreads implementation or build a higher-level synchronization mechanism that enforces the fairness policy your application requires.
Practical Examples
Theory is best understood through practice. We will now construct a complete application on the Raspberry Pi 5 that demonstrates the power and utility of read-write locks. Our example will simulate a common embedded use case: a shared “device status” cache. Multiple threads will read this status frequently to report it, while a single “sensor” thread will update it periodically with new data.
Hardware and Software Setup
- Hardware: Raspberry Pi 5 with Raspberry Pi OS (or any other standard Linux distribution). No external components are required.
- Software: GCC compiler and the Pthreads library. These are included by default in Raspberry Pi OS.
File Structure
We will create a single source file for our application. The project directory will look like this:
/home/pi/rwlock_project/
└── status_monitor.c
└── Makefile
Application Code (status_monitor.c
)
The C code below sets up a global structure DeviceStatus
protected by a pthread_rwlock_t
. We will create three reader threads and one writer thread. The readers will continuously read and print the device status, while the writer will wake up every two seconds to modify it.
// status_monitor.c
// A practical example of Pthreads read-write locks on Raspberry Pi 5.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#define NUM_READERS 3
// This is the shared data structure that our threads will access.
typedef struct {
int temperature;
int humidity;
long last_update_timestamp;
int error_code;
} DeviceStatus;
// Global instance of our shared data and the read-write lock
DeviceStatus g_device_status;
pthread_rwlock_t g_status_lock;
// The function for our reader threads
void *reader_thread_func(void *arg) {
long thread_id = (long)arg;
printf("Reader thread %ld starting.\n", thread_id);
while (1) {
// Acquire a read lock. Multiple readers can hold this lock simultaneously.
pthread_rwlock_rdlock(&g_status_lock);
// --- Start of Read-Only Critical Section ---
printf("Reader %ld: Temp=%dC, Humidity=%d%%, Err=%d (Updated: %ld)\n",
thread_id,
g_device_status.temperature,
g_device_status.humidity,
g_device_status.error_code,
g_device_status.last_update_timestamp);
// --- End of Read-Only Critical Section ---
// Release the read lock
pthread_rwlock_unlock(&g_status_lock);
// Sleep for a short, random interval to simulate work
usleep((rand() % 100 + 1) * 1000); // 1-100 ms
}
return NULL;
}
// The function for our writer thread
void *writer_thread_func(void *arg) {
printf("Writer thread starting.\n");
while (1) {
// Sleep for 2 seconds to simulate periodic updates
sleep(2);
// Acquire a write lock. This will block until all readers have unlocked.
// No other thread (reader or writer) can acquire a lock while we hold this.
pthread_rwlock_wrlock(&g_status_lock);
// --- Start of Write-Only Critical Section ---
printf("\n>>> Writer acquiring lock to update status...\n");
g_device_status.temperature = (rand() % 50) + 10; // 10-59 C
g_device_status.humidity = (rand() % 60) + 30; // 30-89 %
g_device_status.error_code = (rand() % 100 == 0) ? 1 : 0; // 1% chance of error
g_device_status.last_update_timestamp = time(NULL);
printf(">>> Writer finished update. Releasing lock.\n\n");
// --- End of Write-Only Critical Section ---
// Release the write lock
pthread_rwlock_unlock(&g_status_lock);
}
return NULL;
}
int main() {
pthread_t reader_threads[NUM_READERS];
pthread_t writer_thread;
long i;
// Initialize random number generator
srand(time(NULL));
// Initialize the read-write lock with default attributes
if (pthread_rwlock_init(&g_status_lock, NULL) != 0) {
perror("pthread_rwlock_init failed");
return 1;
}
printf("Starting reader and writer threads...\n");
// Create the writer thread
if (pthread_create(&writer_thread, NULL, writer_thread_func, NULL) != 0) {
perror("pthread_create for writer failed");
return 1;
}
// Create the reader threads
for (i = 0; i < NUM_READERS; i++) {
if (pthread_create(&reader_threads[i], NULL, reader_thread_func, (void *)i) != 0) {
perror("pthread_create for reader failed");
return 1;
}
}
// Let the threads run. In a real application, you would join them on shutdown.
// For this demo, we'll just run indefinitely.
pthread_join(writer_thread, NULL);
for (i = 0; i < NUM_READERS; i++) {
pthread_join(reader_threads[i], NULL);
}
// Clean up the lock
pthread_rwlock_destroy(&g_status_lock);
return 0;
}
Code Explanation
DeviceStatus
struct: This is our shared resource. It contains simulated sensor data.- Global Variables: We declare a global instance of
DeviceStatus
and thepthread_rwlock_t
that will protect it. Using global variables simplifies this example, but in larger applications, you would encapsulate these within a class or module. reader_thread_func
: Each reader thread enters an infinite loop. Inside the loop, it first callspthread_rwlock_rdlock()
. This call will only block if the writer thread currently holds the lock. Once the lock is acquired, it safely reads the globalg_device_status
and prints it. Finally, it callspthread_rwlock_unlock()
and sleeps for a random, short duration.writer_thread_func
: The writer thread has a slower loop. It sleeps for two seconds, then callspthread_rwlock_wrlock()
. This call will block if any other thread (reader or writer) holds the lock. Once it acquires the exclusive lock, it updates theg_device_status
with new random values and prints a message indicating it has performed an update. It then callspthread_rwlock_unlock()
, which may allow the waiting reader threads to proceed.main
function: Themain
function initializes the read-write lock usingpthread_rwlock_init()
. It then creates the single writer and multiple reader threads.pthread_join()
is used to wait for the threads to complete, although in this infinite-loop example, it will never be reached. Finally,pthread_rwlock_destroy()
is called for cleanup.
Build and Execution
We will use a simple Makefile
to compile our application.
Makefile
# Makefile for the status_monitor application
CC=gcc
CFLAGS=-Wall -Werror -g
LDFLAGS=-lpthread
TARGET=status_monitor
all: $(TARGET)
$(TARGET): $(TARGET).c
$(CC) $(CFLAGS) -o $(TARGET) $(TARGET).c $(LDFLAGS)
clean:
rm -f $(TARGET)
Tip: The
-Wall
and-Werror
flags are your best friends in C development. They enable all warnings and treat them as errors, catching potential bugs early. The-g
flag includes debugging symbols, which is essential for using tools like GDB. The-lpthread
linker flag is mandatory for linking the Pthreads library.
Compilation and Running
- Open a terminal on your Raspberry Pi 5.
- Navigate to the
rwlock_project
directory. - Run the
make
command to compile the code.pi@raspberrypi:~/rwlock_project $ make gcc -Wall -Werror -g -o status_monitor status_monitor.c -lpthread
- Execute the compiled program:
pi@raspberrypi:~/rwlock_project $ ./status_monitor
Expected Output and Analysis
When you run the program, you will see a rapid stream of output from the reader threads. Notice how the output from different readers is interleaved, demonstrating that they are running concurrently.
Starting reader and writer threads...
Reader thread 0 starting.
Reader thread 1 starting.
Reader thread 2 starting.
Writer thread starting.
Reader 1: Temp=0C, Humidity=0%, Err=0 (Updated: 0)
Reader 0: Temp=0C, Humidity=0%, Err=0 (Updated: 0)
Reader 2: Temp=0C, Humidity=0%, Err=0 (Updated: 0)
Reader 1: Temp=0C, Humidity=0%, Err=0 (Updated: 0)
... (many more reader outputs) ...
Reader 0: Temp=0C, Humidity=0%, Err=0 (Updated: 0)
>>> Writer acquiring lock to update status...
>>> Writer finished update. Releasing lock.
Reader 2: Temp=34C, Humidity=78%, Err=0 (Updated: 1678886402)
Reader 0: Temp=34C, Humidity=78%, Err=0 (Updated: 1678886402)
Reader 1: Temp=34C, Humidity=78%, Err=0 (Updated: 1678886402)
... (readers now show the new values) ...
sequenceDiagram actor R1 as Reader 1 actor R2 as Reader 2 actor W as Writer participant Lock as pthread_rwlock_t participant Data as DeviceStatus R1->>+Lock: pthread_rwlock_rdlock() Lock-->>-R1: OK (Granted) R1->>+Data: Read Status Data-->>-R1: Returns data R1->>+Lock: pthread_rwlock_unlock() Lock-->>-R1: OK R2->>+Lock: pthread_rwlock_rdlock() Lock-->>-R2: OK (Granted) R2->>+Data: Read Status Data-->>-R2: Returns data note right of W: After 2s sleep... W->>+Lock: pthread_rwlock_wrlock() note over Lock: Lock is held by R2.<br/>Writer must wait. R2->>+Lock: pthread_rwlock_unlock() Lock-->>-R2: OK alt Writer Acquires Lock Lock-->>-W: OK (Granted Exclusive) W->>+Data: Update Status Data-->>-W: OK W->>+Lock: pthread_rwlock_unlock() Lock-->>-W: OK end R1->>+Lock: pthread_rwlock_rdlock() note over Lock: Writer has released.<br/>Readers can proceed. Lock-->>-R1: OK (Granted) R1->>+Data: Read (New) Status Data-->>-R1: Returns updated data R1->>+Lock: pthread_rwlock_unlock() Lock-->>-R1: OK
Analysis:
- Concurrent Reads: The initial flurry of output from Readers 0, 1, and 2 shows them accessing the shared data simultaneously. They are not blocking each other.
- Writer Blocking: After two seconds, the writer thread wakes up and calls
pthread_rwlock_wrlock()
. At this point, all reader threads that callpthread_rwlock_rdlock()
will be blocked. You will observe a brief pause in the reader output. The writer prints its “acquiring lock” message, performs the update, and releases the lock. - Resuming Reads: As soon as the writer calls
pthread_rwlock_unlock()
, the waiting reader threads are unblocked. They all acquire the read lock and immediately begin printing the new status values. The system returns to its high-concurrency read state.
This example clearly illustrates the performance benefit: for the entire two-second interval between writes, all three reader threads were able to perform their work in parallel, something that would be impossible with a standard mutex.
Common Mistakes & Troubleshooting
Read-write locks are powerful but introduce unique failure modes that can be challenging to debug. Awareness of these common pitfalls is the first step toward writing robust, correct code.
Exercises
- Experiment with Thread Ratios:
- Objective: Observe the effect of changing the reader-to-writer ratio on application behavior.
- Task: Modify the
NUM_READERS
macro instatus_monitor.c
to 10. Also, change thesleep()
duration in the writer thread to 5 seconds. Recompile and run the application. - Verification: Observe the output. Do you see a much larger volume of read activity between writes? Does the pause for the writer seem more pronounced? Now, change
NUM_READERS
to 1 and the writer’s sleep to 1 second. How does the behavior change? The application should feel much more dominated by write locks.
- Performance Comparison with a Mutex:
- Objective: Quantify the performance difference between a read-write lock and a mutex in a read-heavy scenario.
- Task: Create a copy of
status_monitor.c
namedstatus_monitor_mutex.c
. In the new file, replace thepthread_rwlock_t
with apthread_mutex_t
. Replacepthread_rwlock_init
withpthread_mutex_init
,pthread_rwlock_rdlock
/wrlock
withpthread_mutex_lock
, andpthread_rwlock_unlock
withpthread_mutex_unlock
. Add simple counters in the reader threads to count how many reads they complete. Run both versions for a fixed duration (e.g., 30 seconds) and sum the total reads from all reader threads. - Verification: Compare the total number of reads completed by the
rwlock
version versus themutex
version. Therwlock
version should show a significantly higher read throughput.
- Demonstrate Lock Upgrading Deadlock:
- Objective: Intentionally create a deadlock to understand the lock upgrade problem.
- Task: Create a new program where a single thread function first acquires a read lock. Inside the read-lock critical section, have it attempt to acquire a write lock on the same
rwlock
object. - Verification: Run the program. It should hang indefinitely. Use the GDB debugger (
gdb ./my_program
,run
, thenCtrl+C
, theninfo threads
,thread <id>
,bt
) to inspect the state of the hung thread. The backtrace (bt
) should show the thread is stuck inside thepthread_rwlock_wrlock
call.
- Implement a Thread-Safe Cache:
- Objective: Build a slightly more complex data structure using a read-write lock.
- Task: Design a simple fixed-size cache (e.g., an array of key-value pairs). Implement three functions:
cache_init()
,cache_get(key)
, andcache_put(key, value)
. Thecache_get
function should only require a read lock, as it just searches the array. Thecache_put
function should take a write lock, as it may modify an existing entry or add a new one. Write amain
function that creates multiple threads, some callingcache_get
in a loop and one or two callingcache_put
periodically. - Verification: The program should run without data corruption. Readers should be able to get values concurrently. When a
put
operation occurs, readers should briefly block and then see the newly updated value.
Summary
This chapter provided a comprehensive introduction to read-write locks, a critical tool for optimizing performance in multi-threaded applications with read-dominant workloads.
- Core Concept: Read-write locks provide a shared-exclusive locking mechanism, allowing multiple concurrent readers but only a single exclusive writer.
- Performance: They significantly improve throughput over mutexes in scenarios where data is read far more often than it is written, by eliminating unnecessary serialization of read-only operations.
- Pthreads API: We explored the essential functions for managing
pthread_rwlock_t
objects:pthread_rwlock_init
,pthread_rwlock_destroy
,pthread_rwlock_rdlock
,pthread_rwlock_wrlock
, andpthread_rwlock_unlock
. - Practical Implementation: We successfully built, compiled, and ran a demonstration on the Raspberry Pi 5, observing the concurrent behavior of readers and the exclusive access of the writer in a real-world simulation.
- Common Pitfalls: We identified critical issues like deadlock from lock upgrading, writer starvation, and the importance of proper lock lifecycle management (
init
/destroy
) and ownership.
By mastering read-write locks, you have added a more sophisticated and efficient synchronization primitive to your embedded programming toolkit, enabling you to build faster and more scalable concurrent systems.
Further Reading
- The Single UNIX Specification (POSIX.1-2017): The official standard for Pthreads. The pages for
pthread_rwlock_init
,pthread_rwlock_rdlock
, andpthread_rwlock_wrlock
are the definitive reference. - Linux Manual Pages (man7.org): The
pthreads(7)
man page provides an excellent overview of the Pthreads API available on Linux systems. - “The Linux Programming Interface” by Michael Kerrisk: An exhaustive and highly respected guide to Linux system programming. Chapter 31, “Thread Synchronization,” provides an in-depth discussion of read-write locks.
- LWN.net – “Reader/writer locks and their relationship with RCU”: A more advanced article that discusses the implementation and performance characteristics of read-write locks within the Linux kernel, providing deeper context.
- Butenhof, David R. “Programming with POSIX Threads.”: A classic and authoritative book dedicated entirely to the Pthreads API, offering detailed explanations and rationale behind the design.