校园网站建设成本,捕鱼游戏在哪做网站,医疗 企业 网站制作,建地方门户网站#x1f916;个人主页#xff1a;晚风相伴-CSDN博客 #x1f496;如果觉得内容对你有帮助的话#xff0c;还请给博主一键三连#xff08;点赞#x1f49c;、收藏#x1f9e1;、关注#x1f49a;#xff09;吧 #x1f64f;如果内容有误或者有写的不好的地方的话… 个人主页晚风相伴-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要初始化的互斥量attrnullptr 返回值成功返回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 5int 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 condconst 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);