Linux Thread Synchronization and Mutual Exclusion
I. Core Theoretical Foundations: Mutual Exclusion and Synchronization
1. Mutual Exclusion (Mutex): Exclusive Access to Critical Resources
Core Concepts
- Critical Resource: A resource that needs to be read and written by multiple threads (e.g., global variables, files, hardware devices), which can only be accessed by one thread at a time.
- Mutual Exclusion: Ensures exclusive access to critical resources through locking mechanisms, preventing data inconsistency caused by simultaneous operations from multiple threads.
- Atomic Operation: The code segment from locking to unlocking must be fully executed in one thread scheduling and cannot be interrupted by other threads.
Root Cause of Issues: Interleaved Instruction Execution
Take A++ as an example---its assembly instructions include at least 3 steps:
- Read the value of variable
Ainto a register; - Increment the value in the register by 1;
- Write the result back to variable
A.
If Thread 1 is scheduled to switch after executing the first 2 steps, Thread 2 continues to operateA, resulting in a final value smaller than expected (data consistency corruption).
Steps to Use Mutex
- Define a mutex :
pthread_mutex_t mutex; - Initialize the mutex :
pthread_mutex_init(&mutex, NULL);(NULL indicates default attributes) - Lock :
pthread_mutex_lock(&mutex);(Blocking---waits if the lock is occupied) - Unlock :
pthread_mutex_unlock(&mutex);(Releases the lock and wakes waiting threads) - Destroy the mutex :
pthread_mutex_destroy(&mutex);(Releases resources to avoid memory leaks)
Key Function Explanations
| Function Prototype | Description |
|---|---|
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr) |
Initializes the mutex; attr specifies lock attributes (default: NULL) |
int pthread_mutex_lock(pthread_mutex_t *mutex) |
Blocks to acquire the lock; the thread blocks if the lock is occupied |
int pthread_mutex_trylock(pthread_mutex_t *mutex) |
Non-blocking lock acquisition; returns an error immediately if the lock is occupied (no blocking) |
int pthread_mutex_unlock(pthread_mutex_t *mutex) |
Releases the lock; must be executed by the thread that acquired the lock |
int pthread_mutex_destroy(pthread_mutex_t *mutex) |
Destroys the mutex; must be executed after unlocking |
2. Synchronization (Semaphore): Ordered Resource Access
Core Concepts
- Synchronization: Threads execute in a predetermined order, essentially "mutual exclusion with order constraints"---a special case of mutual exclusion.
- Semaphore: Controls resource access permissions through a counter, supporting cross-thread release (e.g., Thread 1 releases a semaphore to wake Thread 2).
Steps to Use Semaphores
- Define a semaphore :
sem_t sem; - Initialize the semaphore :
sem_init(&sem, pshared, value);pshared = 0: Used between threads;pshared != 0: Used between processes;value: Initial value of the semaphore (0/1 for binary semaphores, ≥1 for counting semaphores).
- P Operation (Request Resource) :
sem_wait(&sem);(Decrements the semaphore by 1; blocks if the value is 0) - V Operation (Release Resource) :
sem_post(&sem);(Increments the semaphore by 1; wakes blocked threads) - Destroy the semaphore :
sem_destroy(&sem);
3. Differences Between Mutex and Semaphore
| Comparison Dimension | Mutex | Semaphore |
|---|---|---|
| Releasing Entity | Must be released by the thread that acquired the lock | Can be released by other threads (cross-wakeup) |
| Resource Count | Only supports binary values (0/1, exclusive resource) | Supports counting (≥0, shared multi-resources) |
| Application Scenario | Exclusive access to critical resources (single resource) | Thread synchronization, concurrent access to multi-resources |
| Sleep Allowed | No sleep in the critical section (avoids deadlocks) | Moderate sleep allowed (e.g., waiting for resources) |
4. Deadlock: A Fatal Trap for Multi-Threads
Four Necessary Conditions for Deadlock (All Must Be Met)
- Mutual Exclusion: Resources can only be occupied by one thread at a time;
- Hold and Wait: A thread holds partial resources while requesting other resources;
- No Preemption: Resources cannot be forcibly seized---only released voluntarily;
- Circular Wait: Multiple threads form a circular resource request chain (e.g., Thread 1 waits for Thread 2's resource, and Thread 2 waits for Thread 1's resource).
II. Practical Code Analysis: From Problems to Solutions
Example 1: Lock-Free Scenario---Resource Contention (01pthreadr.c)
Code Function
Two threads increment the global variable A 5000 times each without lock protection. Observe data inconsistency issues.
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int A = 0; // Critical resource (global variable)
void* th(void* arg) {
int i = 5000;
while(i--) {
int tmp = A; // Step 1: Read A
printf("A is %d\n", tmp+1);
A = tmp + 1; // Step 3: Write back to A
}
return NULL;
}
int main(int argc, char **argv) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, th, NULL);
pthread_create(&tid2, NULL, th, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
Execution Result and Issues
- Expected Result:
Aultimately equals 10000; - Actual Result:
Ais usually less than 10000 (e.g., 9876) with varying results each run. - Cause: The
A++instructions of the two threads execute interleaved, leading to data overwriting.
Example 2: Resolving Resource Contention with Mutex (02pthread_10000.c)
Code Function
Add a mutex to Example 1 to protect the increment operation of global variable A, ensuring data consistency.
c
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
int A = 0;
pthread_mutex_t mutex; // Define mutex
void *thread(void *arg) {
int i = 5000;
while (i--) {
pthread_mutex_lock(&mutex); // Lock (start of critical section)
int temp = A;
printf("A is %d\n", temp+1);
A = temp + 1;
pthread_mutex_unlock(&mutex); // Unlock (end of critical section)
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL); // Initialize mutex
pthread_create(&t1, NULL, thread, NULL);
pthread_create(&t2, NULL, thread, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex); // Destroy mutex
printf("A is %d\n", A); // Final result stably equals 10000
return 0;
}
Key Improvements
- After locking, the three steps of
A++become an atomic operation, avoiding thread interleaving; - Execution Result:
Aultimately equals 10000 stably, ensuring data consistency. - Compilation Command:
gcc 02pthread_10000.c -o lock_demo -lpthread
Example 3: Incorrect Mutex Usage---Deadlock Risk (03lock.c)
Code Function
10 threads compete for 3 counters. Each thread requests all counter locks in sequence to demonstrate a deadlock scenario.
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#define WIN 3 // 3 counters
#define NUM_THREADS 10 // 10 threads
pthread_mutex_t counter_locks[WIN]; // Array of counter locks
typedef struct{
int client_id; // Thread ID
} client_t;
void *client(void *arg){
client_t *client = (client_t *)arg;
int id = client->client_id;
int i=0;
// Error: Each thread requests all counter locks in sequence
for(i = 0; i < WIN; i++){
pthread_mutex_lock(&counter_locks[i]);
printf("Client %d is in critical section (win%d)\n", id, i+1);
sleep(1); // Sleep in the critical section (amplifies deadlock probability)
pthread_mutex_unlock(&counter_locks[i]);
}
pthread_exit(NULL);
}
int main(){
int i = -1;
pthread_t threads[NUM_THREADS];
client_t *clients = malloc(sizeof(client_t) * NUM_THREADS);
// Initialize counter locks
for(i=0; i<WIN; i++) pthread_mutex_init(&counter_locks[i], NULL);
// Create 10 threads
for(i = 0; i < NUM_THREADS; i++){
clients[i].client_id = i;
pthread_create(&threads[i], NULL, client, (void *)&clients[i]);
}
// Join threads
for(i = 0; i < NUM_THREADS; i++){
pthread_join(threads[i], NULL);
}
// Destroy locks
for(i=0; i<WIN; i++) pthread_mutex_destroy(&counter_locks[i]);
free(clients);
return 0;
}
Deadlock Cause
- Thread 1 holds the lock for
win1and requestswin2; Thread 2 holds the lock forwin2and requestswin1, forming a circular wait; - The critical section includes
sleep(1), which prolongs lock holding time and makes deadlock highly likely. - Solution: All threads request locks in a fixed order (e.g., always request
win1first, thenwin2andwin3) to break the circular wait condition.
Example 4: Non-Blocking Lock---Avoiding Deadlocks (04trylock.c)
Code Function
10 threads compete for 3 counters. Use pthread_mutex_trylock for non-blocking lock acquisition to avoid deadlocks.
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
pthread_mutex_t mutex1, mutex2, mutex3; // 3 counter locks
void* th(void* arg) {
int ret = 0;
while (1) {
// Non-blocking request for win1 lock
ret = pthread_mutex_trylock(&mutex1);
if (0 == ret) {
printf("get win1...\n");
sleep(rand() % 5 + 1); // Simulate business processing
printf("release win1...\n");
pthread_mutex_unlock(&mutex1);
break;
}
// Request win2 if win1 acquisition fails
else if ((ret = pthread_mutex_trylock(&mutex2)) == 0) {
printf("get win2...\n");
sleep(rand() % 5 + 1);
printf("release win2...\n");
pthread_mutex_unlock(&mutex2);
break;
}
// Request win3 if win2 acquisition fails
else if ((ret = pthread_mutex_trylock(&mutex3)) == 0) {
printf("get win3...\n");
sleep(rand() % 5 + 1);
printf("release win3...\n");
pthread_mutex_unlock(&mutex3);
break;
}
}
return NULL;
}
int main(int argc, char** argv) {
int i = 0;
srand(time(NULL));
pthread_t tid[10] = {0};
// Initialize locks
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
pthread_mutex_init(&mutex3, NULL);
// Create 10 threads
for (i = 0; i < 10; i++) {
pthread_create(&tid[i], NULL, th, NULL);
}
// Join threads
for (i = 0; i < 10; i++) {
pthread_join(tid[i], NULL);
}
// Destroy locks
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
pthread_mutex_destroy(&mutex3);
return 0;
}
Core Optimizations
- Use
pthread_mutex_trylockfor non-blocking lock acquisition; if the request fails, immediately try the next resource without blocking; - Avoid circular waits between threads, completely resolving deadlock issues;
- Execution Result: 10 threads seize 3 counters in sequence without deadlocks, releasing resources normally.
Example 5: Thread Synchronization with Semaphores (05sem_hw.c)
Code Function
Two threads achieve synchronization through semaphores, alternately outputting "Hello" and "World" (Thread 1 wakes Thread 2 after output, and Thread 2 wakes Thread 1 after output).
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <semaphore.h>
sem_t sem_H, sem_W; // Semaphores: sem_H controls "Hello", sem_W controls "World"
void* th1(void* arg) {
int i = 10;
while(i--) {
sem_wait(&sem_H); // P Operation: Request "Hello" semaphore (initial value = 1)
printf("Hello\n");
fflush(stdout); // Flush buffer to avoid output disorder
sem_post(&sem_W); // V Operation: Release "World" semaphore (wake Thread 2)
}
return NULL;
}
void* th2(void* arg) {
int i = 10;
while(i--) {
sem_wait(&sem_W); // P Operation: Request "World" semaphore (initial value = 0, blocks)
printf("World\n");
sleep(1); // Simulate time-consuming operation
sem_post(&sem_H); // V Operation: Release "Hello" semaphore (wake Thread 1)
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
// Initialize semaphores: sem_H = 1 (Thread 1 can execute first), sem_W = 0 (Thread 2 blocks)
sem_init(&sem_H, 0, 1);
sem_init(&sem_W, 0, 0);
pthread_create(&tid1, NULL, th1, NULL);
pthread_create(&tid2, NULL, th2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// Destroy semaphores
sem_destroy(&sem_H);
sem_destroy(&sem_W);
return 0;
}
Synchronization Logic
- Initial State:
sem_H = 1,sem_W = 0; - Thread 1 successfully requests
sem_H, outputs "Hello", and releasessem_W(sem_W = 1); - Thread 2 successfully requests
sem_W, outputs "World", and releasessem_H(sem_H = 1); - Repeat 10 times to achieve alternating output of "Hello→World".
Example 6: Counting Semaphore---Multi-Resource Sharing (06semcount.c)
Code Function
10 threads compete for 3 counters (multi-resources). Use a counting semaphore to control concurrent access (up to 3 threads can occupy resources simultaneously).
c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <semaphore.h>
sem_t sem_WIN; // Counting semaphore (controls counter resources)
void* th(void* arg) {
sem_wait(&sem_WIN); // P Operation: Request counter resource (semaphore -= 1)
printf("get win...\n");
sleep(rand() % 5 + 1); // Simulate business processing (random time: 1-5 seconds)
printf("release win...\n");
sem_post(&sem_WIN); // V Operation: Release counter resource (semaphore += 1)
return NULL;
}
int main(int argc, char** argv) {
int i = 0;
srand(time(NULL));
pthread_t tid[10] = {0};
// Initialize counting semaphore: initial value = 3 (3 counter resources)
sem_init(&sem_WIN, 0, 3);
// Create 10 threads
for (i = 0; i < 10; i++) {
pthread_create(&tid[i], NULL, th, NULL);
}
// Join threads
for (i = 0; i < 10; i++) {
pthread_join(tid[i], NULL);
}
// Destroy semaphore
sem_destroy(&sem_WIN);
return 0;
}
Core Logic
- The initial value of the semaphore is 3, meaning up to 3 threads can occupy counters simultaneously;
- The semaphore decreases by 1 when a thread requests a resource and increases by 1 when the resource is released;
- When
sem_WIN = 0, subsequent threads block until a thread releases a resource; - Execution Result: At most 3 threads process business simultaneously, with no resource contention.
III. Key Notes and Pitfall Avoidance Guidelines
1. Mutex Usage Specifications
- Minimize Critical Sections : Only include necessary operations (e.g., reading/writing critical resources) after locking; avoid sleep or time-consuming operations (e.g.,
sleep, network requests); - Pair Locking and Unlocking: Avoid deadlocks caused by "locking without unlocking" or crashes from "unlocking without locking";
- Unified Lock Order: In multi-lock scenarios, all threads request locks in a fixed order (e.g., lock A first, then lock B) to break the circular wait condition.
2. Semaphore Usage Specifications
- Distinguish Binary and Counting Semaphores: Binary semaphores (0/1) are used for mutual exclusion; counting semaphores (≥1) are used for multi-resource sharing;
- Reasonable Initial Value Setting: Set the initial value based on the number of resources (e.g., 3 for 3 counters);
- Avoid Semaphore Leaks : Always release resources after requesting them, even if the thread exits abnormally (use
pthread_cleanup_pushto register cleanup functions).
3. Deadlock Prevention Methods
- Break one of the four necessary conditions:
- Avoid circular waits (unified lock order);
- Avoid hold and wait (request all resources at once);
- Allow resource preemption (e.g., use non-blocking locks);
- Weaken mutual exclusion (e.g., use read-write locks to allow multiple reads and single writes).
IV. Summary: Application Scenarios for Mutual Exclusion and Synchronization
| Scenario | Recommended Solution | Core Reason |
|---|---|---|
| Exclusive access to single resources (e.g., global variables) | Mutex | Simple implementation and high efficiency |
| Concurrent access to multi-resources (e.g., multiple counters) | Counting Semaphore | Supports resource quantity control |
| Ordered dependencies between threads (e.g., B can only execute after A) | Semaphore | Flexible synchronization with cross-wakeup |
| Preemption scenarios to avoid deadlocks | Non-blocking Lock (trylock) | Can try other resources if the request fails |
Through the theoretical explanations and code practices in this article, you should have mastered the core mechanisms of Linux thread synchronization. In practical development, select the appropriate synchronization solution based on specific scenarios, strictly follow usage specifications, and avoid deadlocks and resource contention. The core of multi-threaded programming is "safe concurrency"---only by reasonably using mutexes and semaphores can multi-threading truly improve program efficiency.