在多线程编程中,线程同步和互斥是至关重要的概念,用于解决多个线程并发访问共享资源时可能出现的数据不一致问题。上一篇文章我们讨论了基本的概念和一些简单的锁机制,本文将继续深入探讨更高级的同步互斥机制,并结合实际案例分析常见的陷阱和解决方案。尤其是在高并发的场景下,例如 Nginx 反向代理服务器处理大量并发连接请求时,对共享内存的访问就需要非常精细的控制,否则可能导致数据错乱甚至服务崩溃。
互斥锁(Mutex)的进阶使用
死锁的预防与避免
死锁是使用互斥锁时最常见的陷阱之一。当多个线程相互等待对方释放锁时,就会发生死锁,导致所有线程都无法继续执行。以下是一些预防和避免死锁的方法:
- 避免循环等待: 确保线程按照固定的顺序获取锁。如果线程需要同时获取多个锁,则应该按照相同的顺序获取,避免形成循环等待的局面。
- 使用
pthread_mutex_trylock(): 这个函数尝试获取锁,如果锁已经被其他线程持有,则立即返回错误,而不是阻塞等待。线程可以根据返回值来决定是否继续尝试获取锁或执行其他操作。 - 超时机制: 可以设置锁的超时时间,如果线程在指定的时间内无法获取锁,则放弃获取,释放已经持有的锁,避免长时间的阻塞。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
pthread_mutex_t mutex1, mutex2;
void *thread_func1(void *arg) {
printf("线程1: 尝试获取 mutex1\n");
pthread_mutex_lock(&mutex1); // 获取 mutex1
printf("线程1: 成功获取 mutex1\n");
// 模拟一些操作
sleep(1);
printf("线程1: 尝试获取 mutex2\n");
int result = pthread_mutex_trylock(&mutex2); // 尝试获取 mutex2
if (result == 0) {
printf("线程1: 成功获取 mutex2\n");
pthread_mutex_unlock(&mutex2); // 释放 mutex2
} else if (result == EBUSY) {
printf("线程1: mutex2 繁忙,放弃获取\n");
} else {
perror("线程1: pthread_mutex_trylock");
}
pthread_mutex_unlock(&mutex1); // 释放 mutex1
printf("线程1: 释放 mutex1\n");
pthread_exit(NULL);
}
void *thread_func2(void *arg) {
printf("线程2: 尝试获取 mutex2\n");
pthread_mutex_lock(&mutex2); // 获取 mutex2
printf("线程2: 成功获取 mutex2\n");
// 模拟一些操作
sleep(1);
printf("线程2: 尝试获取 mutex1\n");
pthread_mutex_lock(&mutex1); // 获取 mutex1
printf("线程2: 成功获取 mutex1\n");
pthread_mutex_unlock(&mutex1); // 释放 mutex1
printf("线程2: 释放 mutex1\n");
pthread_mutex_unlock(&mutex2); // 释放 mutex2
printf("线程2: 释放 mutex2\n");
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
pthread_create(&thread1, NULL, thread_func1, NULL);
pthread_create(&thread2, NULL, thread_func2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
读写锁(Read-Write Locks)
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这在读操作远多于写操作的场景下非常有用,可以提高并发性能。例如,Nginx 的配置信息经常被读取,但很少被修改,使用读写锁可以显著提高性能。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_rwlock_t rwlock;
int shared_data = 0;
void *reader_thread(void *arg) {
for (int i = 0; i < 5; i++) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("Reader: Shared data = %d\n", shared_data);
pthread_rwlock_unlock(&rwlock); // 释放读锁
sleep(1);
}
pthread_exit(NULL);
}
void *writer_thread(void *arg) {
for (int i = 0; i < 3; i++) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
shared_data++;
printf("Writer: Updated shared data to %d\n", shared_data);
pthread_rwlock_unlock(&rwlock); // 释放写锁
sleep(2);
}
pthread_exit(NULL);
}
int main() {
pthread_t reader1, reader2, writer;
pthread_rwlock_init(&rwlock, NULL);
pthread_create(&reader1, NULL, reader_thread, NULL);
pthread_create(&reader2, NULL, reader_thread, NULL);
pthread_create(&writer, NULL, writer_thread, NULL);
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(writer, NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
条件变量(Condition Variables)
条件变量用于线程间的通知和等待。一个线程可以等待某个条件成立,而另一个线程可以在条件成立时通知等待的线程。这在生产者-消费者模型中非常有用。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int data_ready = 0;
void *consumer_thread(void *arg) {
pthread_mutex_lock(&mutex); // 获取互斥锁
while (!data_ready) {
printf("Consumer: 等待数据...\n");
pthread_cond_wait(&cond, &mutex); // 等待条件变量
}
printf("Consumer: 接收到数据\n");
pthread_mutex_unlock(&mutex); // 释放互斥锁
pthread_exit(NULL);
}
void *producer_thread(void *arg) {
sleep(2);
pthread_mutex_lock(&mutex); // 获取互斥锁
data_ready = 1;
printf("Producer: 数据已准备好\n");
pthread_cond_signal(&cond); // 发送信号通知等待线程
pthread_mutex_unlock(&mutex); // 释放互斥锁
pthread_exit(NULL);
}
int main() {
pthread_t consumer, producer;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
pthread_create(&consumer, NULL, consumer_thread, NULL);
pthread_create(&producer, NULL, producer_thread, NULL);
pthread_join(consumer, NULL);
pthread_join(producer, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
条件变量的虚假唤醒
需要注意的是,条件变量可能会出现虚假唤醒(spurious wakeup),即线程在没有收到通知的情况下被唤醒。因此,在等待条件变量时,应该始终使用 while 循环来检查条件是否真的成立。
信号量(Semaphores)
信号量是一种计数器,用于控制对共享资源的访问。信号量可以用于实现互斥锁和条件变量的功能,但它更通用。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
sem_t empty; // 信号量,表示空闲缓冲区数量
sem_t full; // 信号量,表示已填充缓冲区数量
pthread_mutex_t mutex;
void *producer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
item = rand() % 100; // 生产一个随机数
sem_wait(&empty); // 等待空闲缓冲区
pthread_mutex_lock(&mutex); // 获取互斥锁
buffer[in] = item;
printf("Producer produced item: %d\n", item);
in = (in + 1) % BUFFER_SIZE;
pthread_mutex_unlock(&mutex); // 释放互斥锁
sem_post(&full); // 增加已填充缓冲区数量
sleep(1);
}
pthread_exit(NULL);
}
void *consumer(void *arg) {
int item;
for (int i = 0; i < 10; i++) {
sem_wait(&full); // 等待已填充缓冲区
pthread_mutex_lock(&mutex); // 获取互斥锁
item = buffer[out];
printf("Consumer consumed item: %d\n", item);
out = (out + 1) % BUFFER_SIZE;
pthread_mutex_unlock(&mutex); // 释放互斥锁
sem_post(&empty); // 增加空闲缓冲区数量
sleep(2);
}
pthread_exit(NULL);
}
int main() {
pthread_t producer_thread, consumer_thread;
sem_init(&empty, 0, BUFFER_SIZE); // 初始化 empty 信号量为缓冲区大小
sem_init(&full, 0, 0); // 初始化 full 信号量为 0
pthread_mutex_init(&mutex, NULL);
pthread_create(&producer_thread, NULL, producer, NULL);
pthread_create(&consumer_thread, NULL, consumer, NULL);
pthread_join(producer_thread, NULL);
pthread_join(consumer_thread, NULL);
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
实战避坑经验
- 避免长时间持有锁: 长时间持有锁会降低并发性能,甚至导致其他线程饥饿。应该尽量减少锁的持有时间,只在必要的时候获取锁。例如,在高并发的宝塔面板服务器上,如果数据库操作占用了过多的锁时间,就会影响用户的响应速度。
- 使用合适的同步机制: 不同的同步机制适用于不同的场景。例如,读写锁适用于读多写少的场景,信号量适用于资源计数场景。
- 仔细测试: 并发编程容易出现难以调试的错误。应该编写充分的测试用例,覆盖各种边界情况和异常情况,确保程序的正确性。
- 使用工具进行分析: 可以使用 Valgrind 等工具来检测内存泄漏和数据竞争等问题。
希望本文能够帮助你更好地理解 Linux 下的线程同步和互斥机制,并在实际开发中避免常见的陷阱。
冠军资讯
键盘上的咸鱼