目录
一、什么是死锁
死锁的四个必要条件
如何避免死锁
避免死锁算法
二、Linux线程同步
三 、条件变量
1、条件变量基本原理
2、条件变量的使用
3、条件变量使用示例
为什么 pthread_cond_wait 需要互斥量?
一、什么是死锁
死锁是计算机科学中的一个概念,特别是在操作系统和多线程编程领域中经常遇到。它指的是两个或两个以上的进程或线程在执行过程中,由于互相等待对方持有的资源而无法继续执行的状态。具体来说,每个进程都已经占有了某些资源,但还需要额外的、目前被其他进程所占有的资源才能继续执行。这样,所有涉及的进程都进入了等待状态,形成了一个相互依赖的循环,如果没有外部干预,它们将永远等待下去,无法自行解除阻塞状态。
死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
如何避免死锁
破坏死锁的四个必要条件(破坏其中之一即可)加锁顺序一致避免锁未释放的场景资源一次性分配
避免死锁算法
死锁检测算法 是一种动态检测系统中是否已经发生死锁的方法。它不需要事先采取措施去预防死锁,而是允许系统运行,并定期检查是否有死锁的存在。基本思想是构造一个资源分配图(或称作前驱图),在这个图中,节点代表进程和资源,边表示分配关系和请求关系。如果图中存在环路,则表示系统处于死锁状态。具体步骤包括:
构建资源分配图:图中的节点分为两类,一类代表进程,另一类代表资源类型。从每个进程节点出发的边指向它已分配的资源节点,从每个资源节点出发的边指向请求该资源的进程节点。
检测环路:使用拓扑排序或深度优先搜索等算法检测图中是否存在环。如果存在环,则说明有进程等待的资源被其他在环中的进程所占有,形成死锁。
处理死锁:一旦检测到死锁,系统可以选择采取不同的策略来解决,比如终止某些进程、回滚进程状态或强制释放资源等。
银行家算法 是一种避免死锁的策略,而不是检测死锁。它通过预判分配资源的行为是否安全来避免系统进入不安全状态,从而防止死锁发生。算法核心包括以下几个步骤:
初始化:记录系统中所有可用资源的数量以及每个进程对各类资源的最大需求、已分配资源和当前还需要的资源。
安全性检查:算法在每次分配资源之前,会先检查这次分配是否会导致系统进入不安全状态。这通过试探性地分配资源,然后检查是否存在一个安全序列,即所有进程能够按照某种顺序完成执行,而不会发生某个进程因为缺少资源而无法继续的情况。
资源分配:只有当试探性分配后系统仍处于安全状态时,才会真正分配资源给请求的进程。
资源回收:当进程完成任务后,必须归还所有分配给它的资源,以便其他进程可以使用。
银行家算法的核心在于其预防机制,确保了即使在资源有限的情况下,系统也能保证进程按照某种顺序安全地执行完毕,避免了死锁的发生。
二、Linux线程同步
在Linux环境下,条件变量是线程同步的一种机制,用于实现线程间的协作,使得一个线程能够等待某个条件变为真,而另一个线程负责改变这个条件并通知等待的线程。条件变量通常与互斥锁一起使用,以确保在检查条件和修改条件时的原子性和一致性。
同步(Synchronization)是指在多线程或多进程环境中,协调不同执行单元的操作顺序,确保它们按照预定的方式执行,以避免数据不一致或逻辑错误的问题。同步机制确保了对共享资源的访问是有序的,避免了竞态条件的出现。
竞态条件(Race Condition)是指在多线程程序中,多个线程对同一块数据进行非同步的访问和修改,其最终结果取决于线程的调度顺序。由于线程执行的交错,可能会导致数据不一致、计算错误或者程序行为不符合预期。
三 、条件变量
1、条件变量基本原理
等待条件:当一个线程发现某个条件不满足时,它可以调用pthread_cond_wait()
函数,这会自动释放它之前锁定的互斥锁,并使线程进入等待状态,直到其他线程通过信号机制唤醒它。此时,线程会重新尝试获取互斥锁,并检查条件是否满足,如果不满足则可能再次进入等待状态。
发送信号:当另一个线程改变了条件变量相关的状态,并希望唤醒等待的线程时,它会调用pthread_cond_signal()
或pthread_cond_broadcast()
函数。pthread_cond_signal()
会唤醒一个等待该条件变量的线程(如果有多个线程在等待,则选择其中一个),而pthread_cond_broadcast()
会唤醒所有等待该条件的线程。
2、条件变量的使用
初始化条件变量:可以通过静态初始化或者动态初始化来创建条件变量。
静态初始化使用PTHREAD_COND_INITIALIZER
宏;
动态初始化则使用pthread_cond_init()
函数。
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
锁定互斥锁:在检查或修改条件之前,线程需要先获取互斥锁,以确保操作的原子性和互斥性。
检查条件:线程检查条件是否满足,如果不满足则调用pthread_cond_wait()
进入等待状态。
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
改变条件:在另一个线程中,当条件改变后,应先锁定相同的互斥锁,改变条件,然后调用pthread_cond_signal()
或pthread_cond_broadcast()
来唤醒等待线程。
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
解锁互斥锁:无论是在调用pthread_cond_wait()
前后,还是在改变条件之后,都需要正确地解锁互斥锁。
清理:不再使用条件变量时,动态初始化的条件变量需要通过pthread_cond_destroy()
函数进行清理。
int pthread_cond_destroy(pthread_cond_t *cond)
3、条件变量使用示例
使用条件变量(pthread_cond_t
)和互斥锁(pthread_mutex_t
)的经典示例,实现了线程间的简单同步。使得线程2与线程1交替打印。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1(void *arg)
{
while (1)
{
pthread_cond_wait(&cond,&mutex);
printf("我是线程1\n");
}
}
void *r2(void *arg)
{
while (1)
{
printf("我是线程2\n");
pthread_cond_signal(&cond);
sleep(1);
}
}
int main(void)
{
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, r1, NULL);
pthread_create(&t2, NULL, r2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
}
r1
函数作为线程1的入口点,它在一个无限循环中调用pthread_cond_wait(&cond, &mutex)
。这意味着线程1会释放互斥锁mutex
并阻塞,直到其他线程通过pthread_cond_signal
或pthread_cond_broadcast
唤醒它。一旦被唤醒,它会重新获取互斥锁并打印消息“我是线程1”。
r2
函数作为线程2的入口点,在其循环中打印“我是线程2”,随后调用pthread_cond_signal(&cond)
来唤醒一个等待在cond
上的线程(在这种情况下,通常是线程1)。之后,sleep(1)
让线程2暂停1秒,模拟工作与同步的间隔。
运行结果:
为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
如果先上锁,发现条件不满足,解锁,然后等待在条件变量可以吗?
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
如果在
pthread_mutex_unlock(&mutex);
和pthread_cond_wait(&cond);
之间,其他线程改变了条件并调用了pthread_cond_signal
或pthread_cond_broadcast
,那么这个信号可能会被错过。因为pthread_cond_wait
实际上是在调用时才检查是否应该唤醒线程,而这时线程可能已经错过了信号。竞态条件:在解锁互斥锁后检查条件,然后等待,这期间其他线程可能又修改了条件状态,导致线程可能在不应该等待的情况下进入等待状态,或者即使条件已经满足仍然进入等待。
由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
正确的做法是将条件检查放在pthread_cond_wait
内部,确保在检查条件和等待之间不会错过任何信号。
等待条件代码:
pthread_mutex_lock(&mutex);
while (condition_is_false) { // 条件检查放在循环内
pthread_cond_wait(&cond, &mutex); // 等待时保持互斥锁锁定
}
// 当条件满足时,会从pthread_cond_wait返回
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
这样的设计确保了当线程准备等待时,如果条件已经满足(可能是由于其他线程的操作),它不会错过这一事实,并且可以直接继续执行,避免了信号丢失和不必要的等待。
条件变量和互斥锁的正确配合使用对于避免死锁、竞态条件和信号丢失至关重要。始终遵循“在持有锁的情况下检查条件,然后等待”的原则,确保线程安全和高效的同步。