🤖个人主页:晚风相伴-CSDN博客
💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧
🙏如果内容有误或者有写的不好的地方的话,还望指出,谢谢!!!
让我们共同进步
下一篇《生产者消费者模型》敬请期待
目录
🔥线程间互斥的相关概念
💪互斥量的接口
初始化互斥量
销毁互斥量
互斥量的加锁与解锁
🔥探究互斥量实现原理
可重入函数和线程安全
两者的概念区分
常见的线程不安全和安全情况
可重入与线程安全的联系与区别
☀死锁
产生死锁的四个必要条件
避免死锁
🔥线程同步
条件变量
同步的概念与竞态条件
🔥条件变量接口
初始化
销毁条件
条件等待
唤醒等待
🔥解释pthread_cond_wait中的互斥量
🔥线程间互斥的相关概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源其保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成。
先来看看下面简单实现的抢票的代码
int tickets = 1000;
void* getTickets(void* args)
{
(void)args;
while(true)
{
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
结果演示
💪为什么结果会出现-1呢?
原因:首先要知道一个线程什么时候被调度,调度多长时间,完全是有计算机确定的,程序员决定不了。tickets在进行减减操作时,是分三步的
①读取数据到CPU内的寄存器中
②CPU内部进行计算--
③将结果写回内存中
为了方便叙述,这里给线程编个号
一号线程来了,由于时间片很短执行到第②步就被切走了,二号线程来了,它没有被打断,所以它执行完了这三步,并且这个线程的优先级比较高,一直执行tickets--操作,直到tickets减到1停止,在执行到第①步的时候被切走了,而一号线程回来了,继续从它被打断的地方继续向后执行,也就是从第②步开始继续向后执行,在写回内存后,tickets已经减到了1,但是这个线程又把tickets修改为了999,并且这时它的时间片很长,所以这次又一直将tickets减到了1,由于判断条件tickets不为0,所以tickets继续减减操作,此时tickets减为了0,此时二号线程来了,将0读入到寄存器中进行减减操作,所以结果出现了-1,这就导致了问题的出现。
要解决上面的问题,就需要做到以下三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其它线程进入临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其它线程进入临界区。
要做到以上三点,就需要一把互斥锁,将临界区资源锁住,没有拿到钥匙的线程就不能访问临界区资源,这就能做到保护了临界区资源。Linux上提供的这把互斥锁叫互斥量。
💪互斥量的接口
初始化互斥量
有两种方式初始化互斥量
方法一:全局初始化分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:局部初始化分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
- mutex:要初始化的互斥量
- attr:nullptr
返回值:成功返回0,失败返回错误码
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
- mutex:要销毁的互斥量
返回值:成功返回0,失败返回错误码
销毁互斥量时需要注意
- 使用全局初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量要确保后面的代码中不再有加锁的操作
互斥量的加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码
调用pthread_mutex_lock加锁时,可能会遇到以下情况:
- 互斥量还没被加锁,处于未锁定状态,那么调用该函数会将互斥量加锁锁定。
- 在调用该函数之前,其它线程已经申请了锁,锁定了该互斥量,或者存在其它线程同时竞争式的申请互斥量,但没有竞争到互斥量,那么调用pthread_mutex_lock就会被阻塞,等待会吃两解锁。
所以将上面的抢票代码修改如下:
int tickets = 1000; // 临界资源
class ThreadData
{
public:
ThreadData(string &name, pthread_mutex_t *pmtx) : _tname(name), _pmtx(pmtx)
{
}
public:
string _tname;
pthread_mutex_t *_pmtx;
};
void *getTickets(void *args)
{
ThreadData* td = (ThreadData*)args;
while (true)
{
int n = pthread_mutex_lock(td->_pmtx); // 加锁保护临界区资源
assert(n == 0);
if (tickets > 0)
{
usleep(1000);
printf("%s : %d\n", td->_tname.c_str(), tickets);
cout << td->_tname << " : " << tickets << endl;
tickets--;
n = pthread_mutex_unlock(td->_pmtx);
assert(n == 0);
}
else
{
n = pthread_mutex_unlock(td->_pmtx);
assert(n == 0);
break;
}
// 处理后续的动作
cout << "恭喜,抢票成功" << endl;
usleep(1000);
}
return nullptr;
}
#define THREAD_NUM 5
int main()
{
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr); // 局部定义的锁进行初始化的形式
pthread_t tid[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
ThreadData *td = new ThreadData(name, &mtx);
pthread_create(tid + i, nullptr, getTickets, (void *)td);
}
for (int i = 0; i < THREAD_NUM; i++)
{
pthread_join(tid[i], nullptr);
}
pthread_mutex_destroy(&mtx); // 最后将锁释放掉
return 0;
}
结果演示:
🔥探究互斥量实现原理
加锁的目的是保证操作的原子性。 从汇编的角度来看,如果只有一条汇编语句,我们就认为该汇编语句的执行是原子的, 在汇编中给我们提供了swap或者exchange指令,该指令的作用是将内存中的数据与CPU内寄存器中的数据(CPU内寄存器中的数据也叫做执行流的上下文,寄存器的空间是被所有执行流锁共享的,但是里面的数据是被某一个执行流私有的)进行交换,由于只有一条指令,所以可以保证其原子性。
解锁时会把互斥量变为1。
可重入函数和线程安全
两者的概念区分
线程安全:多个线程并发执行同一段代码时,不会出现不同的结果。
重入:同一个函数被不同的执行流调用,当前一个执行流还没有执行完,就有其它的执行流再次进入该函数,我们称这种情况是重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则称为不可重入函数。
常见的线程不安全和安全情况
不安全情况:
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
安全情况:
- 每个线程对全局变量或者静态变量只有读取权限,而没有写入权限,一般来说这些线程是安全的。
- 类或者接口对于线程来说都是原子操作的
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
可重入与线程安全的联系与区别
联系:
- 函数是可重入的,那就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
区别:
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
- 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个可重入函数的锁还未释放则会产生死锁,因此是不可重入的。
☀死锁
死锁是指子在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占用不会释放的资源而处于的一种永久等待状态。
产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求支援而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能被强行剥夺
- 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
- 对死锁检测
- 银行家算法
🔥线程同步
条件变量
当我们申请临界资源前,要先检测临界资源是否存在,做检测的本质也是在访问临界资源,所以对临界资源的检测一定是要在加锁和解锁之间的。例如一个线程访问队列时,发现队列为空,那么它只能等待,直到其它线程将一个节点添加到队列中,在检测队列是否为空时,如果该线程一直轮询检测,那么势必要频繁的申请锁和释放锁,这样太浪费资源了,那么这种情况就需要用到条件变量了。
因此条件变量可以让线程不在频繁的自己检测了,当第一次检测到条件不满足时就挂起等待,当条件满足时,再通知该线程,让它来申请资源和访问。
同步的概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效的解决了访问临界资源的合理性问题。
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
🔥条件变量接口
初始化
和互斥量那里一样分为全局初始化和局部初始化
局部初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
- cond:要初始化的条件变量
- attr:设置为nullptr即可
返回值:成功返回0,失败返回错误码
全局初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁条件
int pthread_cond_destroy(pthread_cond_t *cond) ;
条件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
- cond:要在这个条件变量上等待
- mutex:互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一批线程
int pthread_cond_signal(pthread_cond_t *cond);//唤醒某个线程
示例代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
#define NUM 4
typedef void (*func_t)(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;
class ThreadData
{
public:
ThreadData(string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
: _name(name), _func(func), _pmtx(pmtx), _pcond(pcond)
{
}
public:
string _name;
func_t _func;
pthread_mutex_t *_pmtx;
pthread_cond_t *_pcond;
};
void func1(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);//线程等待
cout << name << " running... -- 1" << endl;
// sleep(1);
pthread_mutex_unlock(pmtx);
}
}
void func2(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);//线程等待
cout << name << " running... -- 2" << endl;
// sleep(1);
pthread_mutex_unlock(pmtx);
}
}
void func3(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);//线程等待
cout << name << " running... -- 3" << endl;
// sleep(1);
pthread_mutex_unlock(pmtx);
}
}
void func4(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
pthread_cond_wait(pcond, pmtx);//线程等待
cout << name << " running... -- 4" << endl;
// sleep(1);
pthread_mutex_unlock(pmtx);
}
}
void* Entry(void* args)
{
ThreadData* tmp = (ThreadData*)args;
tmp->_func(tmp->_name, tmp->_pmtx, tmp->_pcond);
delete tmp;
return nullptr;
}
int main()
{
pthread_mutex_t mtx;
pthread_cond_t cond;
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t tid[NUM];
func_t funcs[NUM] = {func1, func2, func3, func4};
for (int i = 0; i < NUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);
pthread_create(tid + i, nullptr, Entry, (void*)td);
}
int cnt = 10;
while(cnt)
{
cout << "resume thread run code..." << cnt-- << endl;
pthread_cond_signal(&cond);
// pthread_cond_broadcast(&cond);
sleep(1);
}
cout << "ctrl done" << endl;
quit = true;
pthread_cond_broadcast(&cond);
for(int i = 0; i < NUM; i++)
{
pthread_join(tid[i], nullptr);
cout << "pthread: " << tid[i] << " quit" << endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
结果演示
按照一定的顺序执行。
🔥解释pthread_cond_wait中的互斥量
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去也都不会满足,所以必须还要有一个线程通过某些操作来改变共享变量,使得不满足的条件变得满足,并且友好的通知在条件变量上等待的线程。但是条件不会无缘无故的满足,这必然会牵扯到共享数据的改变。共享数据属于临界资源,因此一定要用互斥锁来保护,没有互斥锁的保护就无法安全的获取和修改共享数据了。
按照上面的说法,我们转换成代码,必须先上锁,检测到条件不满足时,pthread_cond_wait会解锁,然后在条件变量上等待,直到条件满足时,pthread_cond_wait又会重新加锁。
进入pthread_cond_wait函数后,会去检测条件是否满足,如果不满足就把互斥量变为1(解锁),直到条件满足后(pthread_cond_wait返回)将互斥量恢复成原样。
条件变量的规范使用如下:
//等待条件代码
pthread_mutex_lock(&mtx);
while(条件检测)
pthread_cond_wait(&cond, &mtx);
//修改条件
pthread_mutex_unlock(&mtx);
//条件满足,唤醒线程代码
pthread_mutex_lock(&mtx);
//设置条件满足
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);