Hello! Welcome back to our course on C threads.
In the previous lesson, we established the fundamental pattern for thread safety: protecting critical sections with pthread_mutex_lock and pthread_mutex_unlock. We saw that pthread_mutex_lock is a blocking call—if a mutex is unavailable, the thread will pause and wait indefinitely. This is simple and effective for ensuring mutual exclusion.
Today, we'll explore a more flexible, non-blocking approach. This lesson addresses the learning outcome: Use pthread_mutex_trylock for non-blocking lock acquisition strategies. We will cover what pthread_mutex_trylock does, how it differs from its blocking counterpart, and explore two powerful patterns where it is the ideal tool: managing a pool of shared resources and avoiding deadlocks.
The Non-Blocking Alternative: pthread_mutex_trylock
Sometimes, a thread shouldn't just wait. It might have other useful work to do, or it might need to try acquiring a different resource if its first choice is busy. For these scenarios, Pthreads provides pthread_mutex_trylock.
Let's start with a quick conceptual overview of this function.
The video 'Pthread Mutexes' from Udacity offers a concise explanation of pthread_mutex_trylock.
Watch from 02:29 to 03:19. Focus on the key difference described: unlike lock, trylock returns immediately, whether it succeeds or not.
As the video explains, pthread_mutex_trylock attempts to acquire a lock and returns immediately. This behavior is what makes it "non-blocking."
For a more formal definition, let's look at the documentation.
Multithreaded Programming Guide
The 'Multithreaded Programming Guide' from Oracle provides the official specification for pthread_mutex_trylock.
Read the section titled 'Locking a Mutex Without Blocking'. Pay close attention to the function's syntax and, most importantly, its return values. You will see that a return value of 0 means success, while EBUSY indicates the lock was already held.
To summarize the key points:
- Function Signature:
int pthread_mutex_trylock(pthread_mutex_t *mutex); - Behavior:
- If the mutex is unlocked, the function acquires the lock, sets the calling thread as the owner, and returns
0. - If the mutex is already locked (by any thread), the function returns immediately with the error code
EBUSY. The calling thread does not block.
- If the mutex is unlocked, the function acquires the lock, sets the calling thread as the owner, and returns
- Your Code's Responsibility: Your program must check the return value of
pthread_mutex_trylockto determine if the lock was acquired and then act accordingly.
int result = pthread_mutex_trylock(&my_mutex);
if (result == 0) {
// Success! We have the lock.
// --- Start of Critical Section ---
// ... do work with shared data ...
// --- End of Critical Section ---
pthread_mutex_unlock(&my_mutex);
} else if (result == EBUSY) {
// The lock is held by another thread.
// ... do some other work, or try again later ...
} else {
// Some other error occurred.
// ... handle error ...
}
Now, let's explore a practical scenario where this is incredibly useful.
Use Case 1: Managing a Pool of Equivalent Resources
Imagine you have a set of identical resources that threads need to use—for example, a pool of four database connections, or four "stoves" in a kitchen that "chefs" (threads) can use for cooking. A chef doesn't care which stove they get, only that they get an available one.
Using pthread_mutex_lock here would be inefficient. A thread might pick stove #1 and block, waiting for it to become free, even though stoves #2, #3, and #4 are available.
This is a perfect application for pthread_mutex_trylock. A thread can iterate through the resources, trying to lock each one. The first successful trylock gives it a resource to work with, and it can proceed without having to wait.
The following video provides a complete, practical example of this pattern.
Practical example for pthread_mutex_trylock
The video 'Practical example for pthread_mutex_trylock' from CodeVault builds a fantastic analogy with chefs and stoves to demonstrate this pattern from the ground up.
Please watch the segment from 04:47 to 12:44. It's broken down into several parts: The Problem (04:47-06:35): Sets up the scenario with multiple stoves and an array of mutexes. The trylock Loop (06:35-09:23): Shows the core implementation of iterating through mutexes with pthread_mutex_trylock. The Retry Mechanism (09:36-11:50): Extends the logic to handle the case where all resources are busy, by waiting and retrying. Summary (11:50-12:44): Summarizes why trylock is the right tool for this job.
The core logic demonstrated in the video is a powerful and common pattern:
- Setup: Create an array of resources and a corresponding array of mutexes, one for each resource.
pthread_mutex_t stove_mutexes[4]; // Initialize all 4 mutexes... - Acquisition Loop: A thread loops through the mutexes, using
trylockon each one.for (int i = 0; i < 4; i++) { if (pthread_mutex_trylock(&stove_mutexes[i]) == 0) { // Got a lock! printf("Thread %ld is using stove %d\n", pthread_self(), i); // ... use the resource ... pthread_mutex_unlock(&stove_mutexes[i]); // Break the loop since we've done our work. break; } } - Handling Contention: What if all resources are busy? The loop will complete without acquiring a lock. The video shows a clever way to handle this: if the loop finishes unsuccessfully, the thread can sleep for a short duration and then restart the loop from the beginning, effectively polling for an available resource.
Test your understanding!
Imagine in the chef/stove example that you want to prevent a chef from waiting forever if the kitchen is always busy. Modify the logic so that a chef will try to find a stove up to 5 times. If they fail to acquire a stove after 5 full loops through the available stoves, they should give up and print a "Kitchen too busy, going home." message.
How would you modify the control flow to implement this?
Show answer
You would wrap the existing for loop (which iterates through the stoves) inside another loop that counts the retries.
int retries = 0;
while (retries < 5) {
int lock_acquired = 0;
for (int i = 0; i < NUM_STOVES; i++) {
if (pthread_mutex_trylock(&stove_mutexes[i]) == 0) {
// Got a lock
printf("Chef is cooking on stove %d\n", i);
// ... work ...
pthread_mutex_unlock(&stove_mutexes[i]);
lock_acquired = 1;
break; // Exit the inner for-loop
}
}
if (lock_acquired) {
break; // Exit the outer while-loop
}
// If we get here, no stove was available in this pass
printf("All stoves busy, waiting...\n");
retries++;
sleep(1); // Wait a bit before retrying
}
if (retries == 5) {
printf("Kitchen too busy, going home.\n");
}
This nested structure allows the thread to attempt acquiring any of the locks multiple times before finally giving up.
Use Case 2: Deadlock Avoidance
In a future module, we'll cover deadlocks in depth. For now, a simple definition will suffice: a deadlock occurs when two or more threads are blocked forever, each waiting for a resource held by the other. A common cause is acquiring multiple locks in an inconsistent order.
The standard solution is to enforce a strict lock-ordering policy (e.g., always lock mutex A before mutex B). But what if your program's logic makes this difficult? pthread_mutex_trylock offers an alternative.
The document 'Common Concurrency Problems' explains how to use trylock to create a deadlock-free, ordering-robust protocol for acquiring two locks.
Read the section titled 'No Preemption' (pages 9-10). It starts with pthread_mutex_trylock() and provides a code snippet. Focus on understanding the goto pattern: if you can't get the second lock, you must release the first and start over.
The pattern described is as follows: to acquire locks L1 and L2 without risk of deadlock:
- Lock
L1. - Use
trylockonL2. - If
trylock(L2)succeeds, you hold both locks. Proceed. - If
trylock(L2)fails (returnsEBUSY), you must immediately unlockL1and then restart the entire process.
By releasing L1, you break the "hold and wait" condition necessary for deadlock, allowing another thread that might be waiting for L1 to proceed.
This approach, while effective, can introduce a new, related problem called livelock. This happens if two threads repeatedly attempt this sequence, block each other, back off, and retry, without ever making progress. As the reading suggests, introducing a small random delay before retrying is a common way to mitigate this.
Conclusion
You have now added a crucial non-blocking tool to your concurrency toolkit. While pthread_mutex_lock is the workhorse for simple mutual exclusion, pthread_mutex_trylock gives you the flexibility to build more sophisticated and responsive concurrent systems.
Key Takeaways:
pthread_mutex_trylockis a non-blocking function that attempts to acquire a lock and returns immediately.- It returns
0on success andEBUSYif the lock is already held, requiring your code to check the return value. - It is ideal for scenarios with a pool of equivalent resources, allowing a thread to grab the first one available without waiting.
- It provides a powerful strategy for deadlock avoidance by allowing a thread to "back off" and release its locks if it cannot acquire all the locks it needs.
Preview of the Next Lesson:
So far, our threads have communicated only by competing for locks. But what if one thread needs to wait for another thread to complete a task or signal that some data is ready? For example, a "consumer" thread might need to wait for a "producer" thread to add an item to a queue. This requires more than just a mutex. In the next lesson, we will introduce a new synchronization primitive designed for this exact purpose as we learn to initialize and destroy condition variables alongside their associated mutexes.