死锁
在Linux操作系统中,死锁(Deadlock)是指两个或多个进程(或线程)在执行过程中,因互相持有对方所需的资源而又都在等待对方释放资源,导致它们都无法继续执行下去的一种状态。这种僵局会浪费系统资源,甚至可能导致系统崩溃。
案例:
// 线程A和B,以及资源X和Y的初始状态
资源X: 空闲
资源Y: 空闲
线程A: 未持有资源
线程B: 未持有资源
// 线程A获取资源X
资源X: 被线程A持有
资源Y: 空闲
线程A: 持有资源X
线程B: 未持有资源
// 线程B获取资源Y
资源X: 被线程A持有
资源Y: 被线程B持有
线程A: 持有资源X
线程B: 持有资源Y
// 线程A请求资源Y,但资源Y被线程B持有,线程A阻塞
资源X: 被线程A持有
资源Y: 被线程B持有
线程A: 持有资源X,等待资源Y
线程B: 持有资源Y
// 线程B请求资源X,但资源X被线程A持有,线程B阻塞
资源X: 被线程A持有
资源Y: 被线程B持有
线程A: 持有资源X,等待资源Y
线程B: 持有资源Y,等待资源X
// 死锁发生,线程A和B都无法继续执行
造成的原因
死锁通常发生在多进程或多线程环境中,当满足以下四个条件时,就可能发生死锁:
- 互斥条件:一个资源只能被一个进程(线程)访问,即资源独占。
- 占有且等待:进程(线程)在占有一个资源时,可以请求其他资源。
- 不可剥夺条件:一个资源只能由其持有者释放,不能强行剥夺。
- 循环等待条件:多个进程(线程)之间形成一种循环等待资源的关系,每个进程(线程)等待下一个进程(线程)所持有的资源。
解决方式
为了避免和解决死锁,可以采取以下几种方法:
- 资源预分配:在程序设计中尽量避免进程(线程)同时申请多个资源,通过资源预分配降低死锁的可能性。
- 资源有序性:统一规定资源的获取顺序,尽量避免进程(线程)按不同的顺序请求资源。
- 资源剥夺:当一个进程(线程)持有某些资源并请求其他资源时,如果无法满足请求,可以剥夺该进程(线程)之前所持有的资源。
- 死锁检测与恢复:使用算法检测死锁的发生,并进行相应的恢复措施,例如终止某些进程(线程)或回滚操作。
读写锁
读写锁的基本概念
读写锁将线程对共享资源的访问请求分为读请求和写请求两种:
- 读请求:当多个线程发出读请求时,这些线程可以同时执行,共享数据的值可以同时被多个发出读请求的线程获取。
- 写请求:当多个线程发出写请求时,这些线程只能一个一个地执行(同步执行)。此外,当发出读请求的线程正在执行时,发出写请求的线程必须等待前者执行完后才能开始执行;反之亦然。
- 读写锁是一把锁
读写锁的核心特性
- 读锁和写锁的互斥性 :
- 当一个线程持有写锁时,其他线程无法获取读锁或写锁(读写不可以同时进行)。
- 当一个或多个线程持有读锁时,其他线程可以获取读锁,但无法获取写锁
- (写的优先级高于读)。
- 读锁的共享性 :
- 多个线程可以同时持有读锁,而不会相互阻塞。这是因为读取操作不会修改共享资源的数据,因此多个线程同时读取同一个资源不会产生数据竞争。
- 写锁的独占性 :
- 只有一个线程可以持有写锁,其他线程必须等待该线程释放写锁后才能获取写锁。这是因为写入操作会修改共享资源的数据,如果多个线程同时写入同一个资源,就会导致数据不一致。
- 引用计数机制 :
- 读写锁的实现通常采用引用计数的方式。当一个线程获取读锁时,读写锁会记录该线程的引用计数,只有当所有持有读锁的线程都释放读锁后,写线程才能获取写锁。这种方式可以确保在写线程获取写锁之前,所有读取操作都已经完成,从而避免数据竞争。
读写锁的操作
在Linux中,读写锁可以通过多种方式实现,包括但不限于使用POSIX线程(pthread)库中的函数。以下是一些常用的操作:
- 初始化读写锁 :
- 使用
PTHREAD_RWLOCK_INITIALIZER
宏赋值给读写锁变量,或者调用pthread_rwlock_init()
函数进行初始化。
- 使用
- 获取锁 :
- 读锁 :通过
pthread_rwlock_rdlock()
或pthread_rwlock_tryrdlock()
函数获取。前者在锁不可用时会阻塞线程,后者则不会阻塞线程,直接返回错误码。 - 写锁 :通过
pthread_rwlock_wrlock()
或pthread_rwlock_trywrlock()
函数获取。同样,前者在锁不可用时会阻塞线程,后者则不会阻塞线程。
- 读锁 :通过
- 释放锁 :
- 无论是读锁还是写锁,都可以通过
pthread_rwlock_unlock()
函数释放。
- 无论是读锁还是写锁,都可以通过
- 销毁读写锁 :
- 当读写锁不再使用时,可以通过
pthread_rwlock_destroy()
函数将其销毁。
- 当读写锁不再使用时,可以通过
读写锁的应用场景
读写锁特别适用于那些对共享资源进行频繁读取而写入较少的场景。例如,在Web服务器中,缓存数据通常被频繁读取而很少写入,此时使用读写锁可以显著提升并发性能。
线程A加写锁成功,线程B请求读锁:
线程B阻塞
线程A持有读锁,线程B请求写锁 :
线程B阻塞
线程A拥有读写,线程B请求读锁 :
线程B加锁
线程A持有读锁,然后线程B请求写锁,然后线程C请求读锁:
线程B阻塞,线程C阻塞
线程B加锁,线程C阻塞
线程C加锁
线程A持有写锁,然后线程B请求读锁,然后线程C请求写锁 :
线程B阻塞,线程C阻塞
线程C加锁, 线程B阻塞
线程B加锁
互斥锁:读写串行
读写锁:
读:并行
写:串行
举例说明读写锁的具体应用
通过上面的代码可以预防读写的数据紊乱,使其能够正确读操作和写操作
条件变量
条件变量不是锁,可以阻塞线程(但不是什么时候都能阻塞线程)
条件变量通常与互斥锁(mutex)一起使用,以确保线程在访问共享资源或检查条件时的互斥性。
- 条件变量的基本概念
定义:条件变量是线程间同步的一种机制,它给多个线程提供了一个会合的场所。
作用:通过允许线程阻塞和等待另一个线程发送信号的方法,条件变量弥补了互斥锁(Mutex) 的不足。
使用场景:当线程需要等待某个条件成立时,可以使用条件变量来挂起线程,并在条件成立时被唤醒。
- 条件变量的初始化
条件变量在使用前需要进行初始化。Linux中提供了两种初始化方式:
静态初始化:使用宏PTHREAD_COND_INITIALIZER直接初始化静态分配的条件变量。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态初始化:使用pthread_cond_init函数对动态分配的条件变量进行初始化。
pthread_cond_t cond;
pthread_cond_init(&cond, NULL); // NULL表示使用默认属性
- 等待条件变量
线程可以通过调用pthread_cond_wait或pthread_cond_timedwait函数来等待(阻塞)条件变量。
pthread_cond_wait:无限期等待条件变量变为真。
pthread_cond_wait(&cond, &mutex);
在调用此函数之前,线程必须锁定与条件变量关联的互斥锁。函数内部会先解锁互斥锁,然后使线程阻塞在条件变量上。当条件变量被信号唤醒时,线程会重新锁定互斥锁并继续执行。
限时等待:pthread_cond_timedwait:等待条件变量变为真,但设置了超时时间。
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_seconds; // 设置超时时间
pthread_cond_timedwait(&cond, &mutex, &ts);
此函数在达到超时时间或条件变量被信号唤醒时返回。
- 通知条件变量
当条件成立时,可以通过调用pthread_cond_signal或pthread_cond_broadcast函数来通知等待条件变量的线程。
pthread_cond_signal:唤醒 等待条件变量的一个线程。
pthread_cond_signal(&cond);
如果有多个线程在等待条件变量,则由调度策略决定哪个线程被唤醒。
pthread_cond_broadcast:唤醒等待条件变量的所有线程。
pthread_cond_broadcast(&cond);
这会导致所有等待条件变量的线程**都被唤醒,**但它们需要重新竞争互斥锁以访问共享资源。
- 销毁条件变量
当条件变量不再需要时,应使用pthread_cond_destroy函数进行销毁。
pthread_cond_destroy(&cond);
- 注意事项
条件变量的使用必须配合互斥锁,以确保线程在访问共享资源或检查条件时的互斥性。
pthread_cond_wait和pthread_cond_timedwait函数在返回时,都会重新锁定与条件变量关联的互斥锁。
使用条件变量时,应避免唤醒丢失问题,即在条件变量被信号唤醒和线程重新锁定互斥锁之间,条件可能已经不再满足。
例:使用条件变量实现生产者,消费者模型
成功运行
修改后的源码如下
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h> // 引入时间头文件,用于随机数生成
// 定义链表节点结构体
typedef struct node {
int data; // 节点存储的数据
struct node* next; // 指向下一个节点的指针
} Node;
// 全局变量
Node* head = NULL; // 链表头指针,初始为空
pthread_mutex_t mutex; // 互斥锁,用于保护共享资源(链表)
pthread_cond_t cond; // 条件变量,用于线程间同步
// 生产者线程函数
void* produce(void *arg) {
while (1) { // 无限循环,模拟持续生产
Node* pnew = (Node*)malloc(sizeof(Node)); // 分配新节点内存
if (!pnew) { // 内存分配失败检查
perror("malloc failed");
exit(EXIT_FAILURE); // 退出程序
}
pnew->data = rand() % 1000; // 生成0到999之间的随机数作为节点数据
pthread_mutex_lock(&mutex); // 加锁,保护链表操作
pnew->next = head; // 新节点指向当前头节点
head = pnew; // 更新头节点为新节点
printf("Produce: %ld, %d\n", (long)pthread_self(), pnew->data); // 打印生产信息
pthread_cond_signal(&cond); // 发送信号,通知可能等待的消费者线程
pthread_mutex_unlock(&mutex); // 解锁
sleep(rand() % 3); // 生产者休眠随机时间,模拟生产耗时
}
return NULL; // 实际上这个return语句永远不会执行,因为存在无限循环
}
// 消费者线程函数
void* consume(void *arg) {
while (1) { // 无限循环,模拟持续消费
pthread_mutex_lock(&mutex); // 加锁,保护链表操作
while (head == NULL) { // 如果链表为空,则等待
pthread_cond_wait(&cond, &mutex); // 等待条件变量,同时释放锁,并在条件满足时被唤醒时重新获取锁
}
Node* pdel = head; // 取出头节点作为待删除节点
head = head->next; // 更新头节点为下一个节点
printf("Consume: %ld, %d\n", (long)pthread_self(), pdel->data); // 打印消费信息
free(pdel); // 释放已消费节点的内存
pthread_mutex_unlock(&mutex); // 解锁
sleep(rand() % 2); // 消费者休眠随机时间,模拟消费耗时
}
return NULL; // 实际上这个return语句永远不会执行,因为存在无限循环
}
int main() {
pthread_t p1, p2; // 定义两个线程ID
srand(time(NULL)); // 初始化随机数生成器
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_cond_init(&cond, NULL); // 初始化条件变量
pthread_create(&p1, NULL, produce, NULL); // 创建生产者线程
pthread_create(&p2, NULL, consume, NULL); // 创建消费者线程
pthread_join(p1, NULL); // 等待生产者线程结束(注意:这里实际上会导致死锁,因为生产者有无限循环)
pthread_join(p2, NULL); // 等待消费者线程结束(同样,这也会导致死锁)
// 在实际应用中,你可能需要其他机制来优雅地终止线程,比如使用全局变量作为停止标志
pthread_mutex_destroy(&mutex); // 销毁互斥锁
pthread_cond_destroy(&cond); // 销毁条件变量
// 注意:由于生产者和消费者都有无限循环,这里的main函数实际上无法正常退出。
// 为了演示目的,这里保留了原样,但在实际应用中应该避免这种情况。
return 0; // 程序正常结束(但在这个例子中,由于线程的死循环,这行代码实际上不会被执行)
}
信号量
一、概念
在Linux系统中,线程信号量(Semaphore)是一种用于控制多个线程对共享资源访问的同步机制。它本质上是一个非负整数计数器,用于协调多个线程对共享资源的访问。信号量不直接传输数据,而是作为一种许可或限制,确保在任何时刻,对共享资源的访问是安全的、有序的。
帮助理解:
假设有一个公共的水井,多个村民(线程)需要来打水(访问共享资源)。信号量就像是一个计数器,记录了当前水井旁可以容纳多少村民(信号量的值)。
- 当一个村民来打水时(线程访问共享资源),他首先查看信号量的值(调用
sem_wait
)。如果信号量的值大于0(表示水井旁还有空位),则他减去1(进入水井旁打水,信号量值减1),开始打水。如果信号量的值为0(表示水井旁已经满了),则他等待(线程被阻塞)。 - 当一个村民打完水离开时(线程完成共享资源的访问),他通过增加信号量的值(调用
sem_post
)来通知其他等待的村民(唤醒一个阻塞的线程)。
信号量主要用于实现两种功能:
- 互斥(Mutual Exclusion):确保同一时刻只有一个线程能够访问某个共享资源,防止数据竞争和不一致。
- 同步(Synchronization):协调多个线程的执行顺序,确保它们按照预期的顺序访问共享资源或执行特定操作。
二、相关函数
在Linux中,操作信号量的主要函数包括sem_init
、sem_wait
、sem_post
、sem_getvalue
和sem_destroy
等。这些函数定义在<semaphore.h>
头文件中,通常与pthread库一起使用。
- sem_init
- 功能:初始化一个信号量。
- 原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数:
sem
:指向信号量对象的指针。pshared
:指定信号量的作用域。0表示线程间共享,非0表示进程间共享。value
:信号量的初始值。
- 返回值:成功返回0,失败返回-1。
- sem_wait(P操作)
- 功能:将信号量的值减1。如果信号量的值小于等于0,则调用线程将被阻塞,直到信号量的值大于0。
- 原型:
int sem_wait(sem_t *sem);
- 参数:
sem
为指向信号量对象的指针。 - 返回值:成功返回0,失败返回-1。
- sem_post(V操作)
- 功能:将信号量的值加1。如果有线程因为
sem_wait
调用而阻塞在该信号量上,则其中一个线程将被唤醒。 - 原型:
int sem_post(sem_t *sem);
- 参数:
sem
为指向信号量对象的指针。 - 返回值:成功返回0,失败返回-1。
- 功能:将信号量的值加1。如果有线程因为
- sem_getvalue
- 功能:获取信号量的当前值。
- 原型:
int sem_getvalue(sem_t *sem, int *sval);
- 参数:
sem
:指向信号量对象的指针。sval
:用于存储信号量当前值的指针。
- 返回值:成功返回0,失败返回-1。
- sem_destroy
- 功能:销毁一个信号量,释放其资源。
- 原型:
int sem_destroy(sem_t *sem);
- 参数:
sem
为指向信号量对象的指针。 - 返回值:成功返回0,失败返回-1。
通过这种方式,信号量确保了任何时刻对水井的访问都是有序的,避免了冲突和混乱。
使用信号量知识点生成上个案例同样的功能
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
#define BUFFER_SIZE 10
typedef struct node {
int data;
struct node* next;
} Node;
Node* head = NULL;
Node* tail = NULL;
// 信号量:用于控制队列的容量
sem_t empty;
// 信号量:用于同步生产者和消费者
sem_t full;
// 互斥锁:保护队列的修改
pthread_mutex_t mutex;
void* produce(void *arg) {
while (1) {
// 等待队列有空位
sem_wait(&empty);
// 加锁以保护队列
pthread_mutex_lock(&mutex);
// 创建新节点
Node* pnew = (Node*)malloc(sizeof(Node));
if (!pnew) {
perror("malloc failed");
pthread_mutex_unlock(&mutex);
sem_post(&empty); // 释放信号量,因为实际上没有增加队列中的元素
continue;
}
pnew->data = rand() % 1000;
// 将新节点添加到队列尾部
if (tail == NULL) {
head = tail = pnew;
pnew->next = NULL;
} else {
tail->next = pnew;
tail = pnew;
pnew->next = NULL;
}
// 解锁
pthread_mutex_unlock(&mutex);
// 通知消费者队列中有数据了
sem_post(&full);
printf("Produce: %d\n", pnew->data);
sleep(rand() % 3);
}
return NULL;
}
void* consume(void *arg) {
while (1) {
// 等待队列中有数据
sem_wait(&full);
// 加锁以保护队列
pthread_mutex_lock(&mutex);
// 从队列头部取出节点
Node* pdel = head;
if (head == NULL) {
// 理论上不应该发生,因为full信号量保证了队列不为空
pthread_mutex_unlock(&mutex);
sem_post(&empty); // 释放信号量,因为实际上没有消费任何数据
continue;
}
head = head->next;
if (head == NULL) {
tail = NULL; // 如果队列为空,则尾指针也为空
}
// 解锁
pthread_mutex_unlock(&mutex);
// 消费数据
printf("Consume: %d\n", pdel->data);
free(pdel);
// 通知生产者队列中有空位了
sem_post(&empty);
sleep(rand() % 2);
}
return NULL;
}
int main() {
pthread_t p1, p2;
sem_init(&empty, 0, BUFFER_SIZE); // 初始时队列为空,有BUFFER_SIZE个空位
sem_init(&full, 0, 0); // 初始时队列中没有数据
pthread_mutex_init(&mutex, NULL);
srand(time(NULL));
pthread_create(&p1, NULL, produce, NULL);
pthread_create(&p2, NULL, consume, NULL);
// 假设我们不想在这里等待线程结束,可以注释掉以下两行
// pthread_join(p1, NULL);
// pthread_join(p2, NULL);
// 注意:在实际应用中,你应该等待线程结束或者采取其他措施来避免程序过早退出
// 清理资源
// 注意:由于我们注释掉了pthread_join,这里的清理代码可能不会执行,或者执行时线程可能还在运行
// sem_destroy(&empty);
// sem_destroy(&full);
// pthread_mutex_destroy(&mutex);
return 0;
}