概念:多个线程(执行流)同时对临界资源进行访问而不会造成数据二义
实现:同步 + 互斥
同步:对临界资源访问的时序合理性
互斥:同一时间访问的唯一性
1.定义互斥锁变量
pthread_mutex_t mutex
2.对互斥锁变量进行初始化
pthread_mutex_init(&mutex,&attr)
参数:mutex:要初始化的互斥量
attr:互斥量属性,常置NULL
3.对临界资源进行加锁保护
pthread_mutex_lock(&mutex)
//这是一个阻塞加锁,可以加锁就加锁,不可以就等待
pthread_mutex_trylock //非阻塞加锁,尝试加锁,不成功直接报错返回
pthread_mutex_timedlock /非阻塞加锁,一直尝试加锁,到了超时时间还没成功就报错返回
4.对临界资源操作完毕后解锁
pthread_mutex_unlock(&mutex);
注意:用户在加锁之后,需要在任意有可能退出线程的地方进行解锁
5.销毁互斥锁
pthread_mutex_destroy(&mutex);
销毁互斥锁时的注意事项:
1.使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
2.不要销毁一个已经加锁的互斥量
3.已经销毁的互斥量,要确保后面不会有线程再尝试加锁
黄牛抢票的例子
#include
#include
#include
pthread_mutex_t mutex;//定义互斥锁
int ticket = 100;
void *yellow_bull(void *arg)//入口函数
{
while(1) {
pthread_mutex_lock(&mutex);//加锁
if (ticket > 0) {
usleep(1000);
printf("bull %d get a ticket:%d\n", (int)arg, ticket);//打印票号
ticket--;
}else {
printf("have no tickets, bull %d exit\n", (int)arg);//没票时退出
//用户在加锁之后,需要在任意有可能退出线程的地方进行解锁
//没票了如果直接退出则下面无法解锁,其他线程一直等待,无法退出
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);//解锁
}
}
int main()
{
pthread_t tid[4];//创建四个线程
pthread_mutex_init(&mutex, NULL);//初始化互斥锁
int i;
for (i = 0; i < 4; i++) {
int ret = pthread_create(&tid[i], NULL, yellow_bull, (void*)i);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
pthread_join(tid[i], NULL);//线程循环等待
}
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
//mutex: 互斥锁变量
pthread_mutex_destroy(&mutex);//销毁互斥锁
return 0;
}
死锁
概念:多个线程对锁资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使得程序无法继续往下运行
死锁产生的四个条件
1.互斥条件- - -一个锁只有一个线程可以获取
2.不可剥夺条件- - -我加的锁别人不能解
3.请求与保持条件- - -拿着A锁,去请求B锁,但是获取不到B锁,也不释放A锁
4.环路等待条件- - -我拿着A锁请求B锁,对方拿着B锁请求A锁
死锁预防:破坏四个必要条件
死锁避免:银行家算法
银行家算法:
银行家算法是一个用来避免系统进入死锁状态的算法,用它可以判断系统的安全性,如果系统当前处于安全状态,则可以为申请资源的进程分配资源,如果不是安全状态,则不能为申请资源的进程分配资源。
银行家算法执行过程中,首先判断申请资源的进程所申请的资源数目是否合法,若是合法的,则可以为其进行试分配,再利用安全性算法求出安全序列,·如果存在安全序列,则说明可以给申请资源的进程分配资源,分配成功,继续为其它进程服务。如果找不到安全序列,则说明为该进程分配资源后系统会进入不安全状态,所以不能为该进程分配资源,使该进程进入阻塞状态。若申请资源的进程申请的资源数目不合法,则不需要进行试分配,直接使其进入阻塞状态,处理其他申请资源的进程。
参考自这里
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
线程之间实现同步:等待 + 唤醒
操作不满足条件则等待,其他线程使条件满足之后唤醒
条件变量实现同步:
线程对临界资源访问之前,先判断是否能够操作,若可以操作则线程直接操作,若不能,则条件变量提供等待功能,让pcb等待在队列上,其他线程促使条件满足,然后唤醒条件变量等待队列上的线程
注意:
条件变量只提供等待和唤醒功能,具体何时等待,何时唤醒,需要用户自己判断
条件变量函数
1.定义条件变量
pthread_cond_t cond
2.条件变量初始化
pthread_cond_init(&cond,&attr);
参数cond:要初始化的条件变量
attr:通常置NULL
3.用户在判断条件不满足时提供等待功能
pthread_cond_wait(&cond,&mutex);
参数cond:要在这个条件变量上等待
mutex:互斥量
4.用户在促使条件满足后,唤醒等待
pthread_cond_signal(&cond) 唤醒所有等待的线程
pthread_cond_broadcast(&cond) 唤醒至少一个线程
5.销毁条件变量
pthread_cond_destroy(&cond)
为什么pthread_ cond_ wait 需要互斥量?
线程何时等待需要一个判断条件,这个判断条件也是临界资源,等待后其他线程需要促使这个条件满足(修改临界资源),所以一定要用互斥锁来保护。没 有互斥锁就无法安全的获取和修改共享数据。
条件变量的条件判断应该是一个循环判断:
拿厨师做面,顾客来吃面举例(厨师和顾客都是多个线程)
多个顾客线程若同时被唤醒,只有一个顾客可以加锁吃面,其他顾客线程将阻塞在加锁上,而不是条件变量的等待队列上,第一个加锁的顾客吃完面后解锁,这时获取到时间片加锁的可能是另一个顾客线程,顾客如果直接去吃面的话,发现面已经被上一个顾客吃掉了,所以存在逻辑错误,应该在吃面之前先判断是否有面
不同的对象应该放在不同的等待队列
如果一个厨师做了一碗面,同时唤醒所有的厨师和顾客线程,这时另一个厨师拿到时间片后加锁,判断有面,再次等待,进入循环判断,一直都有面,一直等待,但是在等待之前并没有唤醒顾客,导致程序卡死,没人吃面,也没人做面,所以不同的对象应该放在不同的等待队列,用两个条件变量
代码实现
* 以顾客吃面和厨师做面为例子:
* 顾客想要吃面,前提是有面,没有面则等待
* 厨师做面,做好面后,唤醒吃面的人
================================================================*/
#include
#include
#include
int _have_noodle = 0;
pthread_mutex_t mutex;//定义一个互斥锁
pthread_cond_t cond_eat;//定义顾客条件变量
pthread_cond_t cond_cook;//定义厨师条件变量
void *eat_noodle(void *arg)
{
while(1) {//拿到锁之后要先判断有没有面
pthread_mutex_lock(&mutex);
while (_have_noodle == 0) {
//休眠之前应该先解锁
//pthread_cond_wait实现了三步操作:
// 1. 解锁
// 2. 休眠
// 3. 被唤醒后加锁
// 其中解锁和休眠操作必须是原子操作,不可被打断
pthread_cond_wait(&cond_eat, &mutex);//等待在顾客队列
}
//能走下来表示have_noolde==1 ,表示有面
printf("eat noodle\n");
_have_noodle = 0;
pthread_cond_signal(&cond_cook);//唤醒厨师
pthread_mutex_unlock(&mutex);//解锁
}
return NULL;
}
void *cook_noodle(void *arg)
{
while(1) {//拿到锁之后要先判断有没有面
pthread_mutex_lock(&mutex);
while (_have_noodle == 1) {
//现在有面,但是没人吃,不能继续做了,等待在厨师队列
pthread_cond_wait(&cond_cook, &mutex);
}
printf("cook noodle\n");
_have_noodle = 1;
pthread_cond_signal(&cond_eat);//有面了,唤醒吃面的人
pthread_mutex_unlock(&mutex);//解锁
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
pthread_mutex_init(&mutex, NULL);
//int pthread_cond_init(pthread_cond_t *cond,
// const pthread_condattr_t *attr);
pthread_cond_init(&cond_eat, NULL);
pthread_cond_init(&cond_cook, NULL);
//四个吃面和四个厨师
for (int i = 0; i < 4; i++) {
int ret = pthread_create(&tid1, NULL, eat_noodle, NULL);
if (ret != 0) {
printf("pthread create error\n");
return -1;
}
}
for (int i = 0; i < 4; i++) {
int ret = pthread_create(&tid2, NULL, cook_noodle, NULL);
if (ret != 0) {
printf("pthread create error\n");
return -1;
}
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex);
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&cond_cook);
pthread_cond_destroy(&cond_eat);
return 0;
}
遵循POSIX标准,支持平台移植
功能:实现线程之间的同步与互斥
本质:一个计数器(判断当前是否可以对临界资源进行操作)+等待队列+等待+唤醒
原理:
互斥原理:只具有0/1计数时,就可以实现互斥
初始计数为1,表示当前只有一个线程能够获取资源,获取资源之后-1;计数为0,其他线程在等待队列上等待,临界资源被操作完毕之后计数+1,并且唤醒等待队列上的线程
同步原理:对程序逻辑进行控制(对临界资源合理操作控制)
通过计数判断当前能否对临界资源进行操作,不能操作(计数<=0),则等待;其他线程操作后计数+1,促使再次判断,唤醒等待的线程
条件变量与信号量实现同步的区别
1.信号量并不需要搭配互斥锁的使用
2.信号量本身的计数就是是否能够对临界资源进行操作的判断条件;但是条件变量需要用户判断
sem信号量的使用流程:
1.定义信号量
sem_t
2.初始化信号量
int sem_init(sem_t *sem,int pshared,unsigned int value);
sem:信号量变量
pshared:0-表示用于线程间同步与互斥;非0表示进程间同步与互斥
value:信号量计数器初始值
3.判断计数是否可以对临界资源进行操作
若计数>0,函数立即正确返回,返回之前计数-1,其他线程可以继续操作临界资源;
若计数<=0,认为当前不能对临界资源进行操作,线程等待
int sem_wait(sem_t *sem);
sem:信号量变量
4.计数+1,唤醒等待队列上的线程
int sem_post(sem_t *sem);
sem:信号量变量
5.销毁信号量
int sem_destroy(sem_t *sem);
sem:信号量变量
sem信号量的简单操作
#include
#include
#include
#include
#include //注意sem的头文件
int ticket = 100;
sem_t sem;//定义信号量
void *thr_start(void *arg)//线程入口函数
{
while(1) {
//int sem_wait(sem_t *sem);
// 若当前计数<=0 ;则线程一直等待
sem_wait(&sem);//同加锁和解锁的位置一样
if (ticket > 0) {
printf("get a ticket:%d\n", ticket);
ticket--;
}else {
sem_post(&sem);//解锁
pthread_exit(NULL);
}
//int sem_post(sem_t *sem)
//计数+1,促使其它线程操作条件满足,然后唤醒所有等待线程
sem_post(&sem);
}
return NULL;
}
int main ()
{
int i, ret;
pthread_t tid[4];//创建四个线程
//信号量初始化
//int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_init(&sem, 0, 1);
for (i = 0; i < 4; i++) {
ret = pthread_create(&tid[i], NULL, thr_start, NULL);
if (ret != 0) {
printf("pthread create error\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
pthread_join(tid[i], NULL);
}
//int sem_destroy(sem_t *sem);
sem_destroy(&sem);
return 0;
}
单例模式属于常见设计模式的一种
设计模式是大佬们针对经典的常见的场景设计出的对应的解决方案
单例模式的特点
一个对象只能被实例化一次(一个资源只能被加载一次)
单例模式的实现:
饿汉方式:资源在初始化时进行加载
特点:在程序初始化时资源加载时间较长,但之后运行流畅
class info{
public:
static int _data;
int* getdata(){
return &_data;
}
};
int info::data_data=10; //静态成员在类外初始化
int main(){
info a;
info b;
a._data=20;
std::out<<"a.data:"<<*a.getdata()<
输出全为20,因为a和b引用的是同一个对象
懒汉方式:资源在使用时才加载
特点:资源在使用时才被加载,在多个执行流中就可能被加载多次;因此资源的加载和对象的实例化过程需要受保护,线程安全的单例模式主要针对的就是懒汉方式的实现
class info{
public:
static int *_data;
int* getdata(){
pthread_mutex_lock(&mutex);
if(_data==NULL){//判断是否被加载过
_data=new int;
*_data=10;
}
pthread_mutex_unlock(&mutex);
return _data;
}
};
int* info::data_data=NULL;
int main(){
info a;
info b;
std::out<<"a.data:"<<*a.getdata()<