在上一篇文章中,我们探讨了 Linux 下线程同步和互斥的基本概念和使用方法,例如互斥锁(mutex)、条件变量(condition variable)等。本文作为《线程同步和互斥》的下篇,将重点关注多线程编程中常见的死锁问题,以及如何利用各种技巧和工具来提升并发程序的性能。
死锁的产生与避免
死锁是指两个或多个线程互相持有对方需要的资源,导致所有线程都无法继续执行下去的局面。这是多线程编程中最令人头疼的问题之一,尤其是在复杂的系统中。
死锁产生的原因:
- 互斥条件:线程对所分配到的资源进行排他性使用,即一个资源只能被一个线程占用。
- 占有且等待条件:线程已经保持了至少一个资源,但又提出新的资源请求,而该资源已被其他线程占用。
- 不可剥夺条件:线程已获得的资源在未使用完之前不能被抢占,只能由线程自己释放。
- 环路等待条件:发生死锁时,必然存在一个线程资源的环形链,即线程A等待线程B占用的资源,线程B等待线程C占用的资源,依此类推,直到线程N等待线程A占用的资源。
避免死锁的策略:
- 避免多个锁:尽量减少一个线程需要同时持有的锁的数量。如果可能,将多个锁合并成一个锁。
- 锁的顺序一致:确保所有线程都按照相同的顺序获取锁。这是最常用的避免死锁的方法。
- 超时机制:使用带有超时功能的锁,例如
pthread_mutex_timedlock()。如果线程在指定时间内无法获得锁,就放弃并释放已经持有的锁。 - 资源分配避免环路等待:设计系统时,尽量避免资源之间的环形依赖关系。
示例代码:锁顺序一致性
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex1, mutex2;
void* thread_func(void* arg) {
int thread_id = *(int*)arg;
if (thread_id == 0) {
pthread_mutex_lock(&mutex1); // 获取锁1
printf("Thread 0: Acquired mutex1\n");
pthread_mutex_lock(&mutex2); // 获取锁2
printf("Thread 0: Acquired mutex2\n");
// 临界区操作
pthread_mutex_unlock(&mutex2); // 释放锁2
pthread_mutex_unlock(&mutex1); // 释放锁1
} else {
pthread_mutex_lock(&mutex1); // 获取锁1
printf("Thread 1: Acquired mutex1\n");
pthread_mutex_lock(&mutex2); // 获取锁2
printf("Thread 1: Acquired mutex2\n");
// 临界区操作
pthread_mutex_unlock(&mutex2); // 释放锁2
pthread_mutex_unlock(&mutex1); // 释放锁1
}
pthread_exit(NULL);
}
int main() {
pthread_t thread1, thread2;
int thread_ids[2] = {0, 1};
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
pthread_create(&thread1, NULL, thread_func, &thread_ids[0]);
pthread_create(&thread2, NULL, thread_func, &thread_ids[1]);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
排查死锁的工具:gdb 和 pstack
当程序发生死锁时,可以使用 gdb 来查看线程的堆栈信息,从而定位死锁发生的位置。另外, pstack 工具也可以打印进程中所有线程的堆栈,方便快速定位问题。
性能优化:减少锁竞争
锁的使用会带来性能损耗,过度使用锁会降低并发程序的性能。因此,减少锁竞争是提升并发程序性能的关键。
减少锁竞争的策略:
- 减小锁的粒度:将一个大锁分解成多个小锁,从而减少锁的竞争。例如,可以使用哈希表来存储数据,并为每个哈希桶分配一个锁。这种方法被称为分段锁(Segmented Locking)。
- 读写分离:使用读写锁(
pthread_rwlock_t)来允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这可以显著提升读取密集型应用的性能。 - 使用原子操作:对于简单的计数器或标志位等共享变量,可以使用原子操作(atomic operations)来避免锁的使用。原子操作是 CPU 提供的硬件级别的操作,可以保证操作的原子性。
- 无锁数据结构:使用无锁数据结构(lock-free data structures),例如无锁队列、无锁哈希表等。这些数据结构使用 CAS(Compare and Swap)等原子操作来实现并发访问,避免了锁的使用。但需要注意无锁数据结构的实现通常比较复杂,需要仔细测试和验证。
- 使用线程池: 合理配置线程池大小,避免频繁创建和销毁线程,减少上下文切换带来的开销。
示例代码:读写锁
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int shared_data = 0;
pthread_rwlock_t rwlock;
void* reader_func(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); // 释放读锁
usleep(100000); // 模拟读取操作
}
pthread_exit(NULL);
}
void* writer_func(void* arg) {
for (int i = 0; i < 3; i++) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
shared_data++;
printf("Writer: Shared data incremented to %d\n", shared_data);
pthread_rwlock_unlock(&rwlock); // 释放写锁
usleep(200000); // 模拟写入操作
}
pthread_exit(NULL);
}
int main() {
pthread_t reader1, reader2, writer;
pthread_rwlock_init(&rwlock, NULL);
pthread_create(&reader1, NULL, reader_func, NULL);
pthread_create(&reader2, NULL, reader_func, NULL);
pthread_create(&writer, NULL, writer_func, NULL);
pthread_join(reader1, NULL);
pthread_join(reader2, NULL);
pthread_join(writer, NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
实战避坑:Nginx worker 进程同步
在 Nginx 这样的高性能服务器中,worker 进程之间的同步通常使用共享内存和信号量来实现。例如,多个 worker 进程可能需要共享一个缓存,这时就需要使用锁来保护共享内存的访问。同时也要考虑到 Nginx 本身的事件驱动模型,尽量避免阻塞操作,可以使用非阻塞锁或者事件通知机制。
例如,使用 ngx_shmtx_lock() 和 ngx_shmtx_unlock() 来进行共享内存的互斥访问。如果需要进程间通信,可以使用 ngx_event_pipe() 函数创建管道,并使用事件通知机制来避免阻塞。
此外,需要根据具体的业务场景选择合适的锁策略。例如,在高并发场景下,可以考虑使用自旋锁来减少上下文切换的开销。但需要注意,自旋锁可能会导致 CPU 占用率过高,需要合理设置自旋次数。
在配置 Nginx 时,可以通过调整 worker_processes 和 worker_connections 来优化并发性能。同时,也要注意监控 Nginx 的状态,例如使用 ngx_http_stub_status_module 模块来查看并发连接数、请求处理速度等指标,及时发现并解决性能瓶颈。
总的来说,Linux 下的线程同步和互斥是一个复杂而重要的课题。只有深入理解其原理,并结合具体的应用场景,才能编写出高效、稳定的并发程序。希望本文能对你有所启发。
冠军资讯
代码一只喵