定制型网站建设推广,百度收录是什么意思,保险网,营销网站制作公司线程互斥 1.线程互斥2.可重入VS线程安全3.常见锁的概念 喜欢的点赞#xff0c;收藏#xff0c;关注一下把#xff01; 1.线程互斥
到目前为止我们学了线程概念#xff0c;线程控制接下来我们进行下一个话题#xff0c;线程互斥。
有没有考虑过这样的一个问题#xff0c… 线程互斥 1.线程互斥2.可重入VS线程安全3.常见锁的概念 喜欢的点赞收藏关注一下把 1.线程互斥
到目前为止我们学了线程概念线程控制接下来我们进行下一个话题线程互斥。
有没有考虑过这样的一个问题既然线程一旦被创建几乎所有资源都是被所有线程共享的。 那多个线程访问同一份共享资源有没有什么问题
下面我们模拟一下抢票的场景看到底有没有什么问题
#include iostream
#include pthread.h
#include unistd.h
#include string//共享资源火车票
int ticket 10000;void *GetTicket(void *args)
{string name static_castconst char *(args);while (true){if (ticket 0){cout name 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间usleep(1000);}else{break;}}
}int main()
{pthread_t t1,t2,t3,t4;pthread_create(t1,nullptr,GetTicket,(void*)thread-1);pthread_create(t2,nullptr,GetTicket,(void*)thread-2);pthread_create(t3,nullptr,GetTicket,(void*)thread-3);pthread_create(t4,nullptr,GetTicket,(void*)thread-4);pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}这好像没出问题啊到0就停止了。
我们想看到的现象是抢到负数票那怎么实现呢 既然想看到抢到负数票就需要尽可能的让多个线程交叉执行。 多个线程交叉执行的本质就是让调度器尽可能的频繁发生线程调度与切换。 线程一般在时候发生切换呢 时间片到了来了更高优先级的线程线程等待的时候。 那线程是什么时候检测上面的问题呢 从内核态返回用户态的时候线程要对调度状态进行检测如果可以就直接发生线程切换。
那修改一下代码
void *GetTicket(void *args)
{string name static_castconst char *(args);while (true){if (ticket 0){//线程进来之后先休眠要被切走usleep(1000);cout name 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间//usleep(1000);}else{break;}}
}出问题了放了10000张票结果抢到了10002张票。现象就是这个样子。那为什么会出现这样的问题 所谓判断的本质逻辑 1.读取内存数据到CPU内部寄存器中 2.进行判断
所以ticket1多个线程可以同时执行这个判断语句。对不对 答案是不对的。 我们只有一个CPU只有一份寄存器不能同时判断但是注意我们写了usleep语句线程是要被切换走的但是寄存器中的内容是属于这个线程的因此1也要被切走 剩下的线程就可以开始竞争执行if语句判断但很不幸最终都是进去后先休眠。 当线程1被唤醒恢复上下文执行到ticket- -; ticket- -有三个步骤1.读取数据2.更改数据3.写回数据 此时线程1从内存中读取ticke还是1最后写回内存ticket为0 线程2此时醒来恢复上下文它不知道内存中ticket此时是0往下执行到ticket- -此时取到ticket是0最后写回内存中ticket是-1 线程3也醒来和上面一样执行到ticket- -从内存取到数据为-1写回内存中是-2 就是因为我们判断和更新分开而在中间发生了大量的线程切换最终可能出现ticket本来就是1了但是你却放了大量线程同时进来对ticket变量做减减操作进而导致我们的数据出现了负数的情况
那没有判断多线程单纯对一个全局变量进行修改是安全的吗 假设ticket初始是1000多个线程进行执行ticket- -操作 假设刚开始threadA执行。 补充 对变量进行或者- - 在C/C上看起来只有一条语句但是汇编之后至少是三条语句 1.从内存读取数据到CPU寄存器中 2.在寄存器中让CPU进行对应的算逻运算 3.写回到新的结果到内存中变量的为止 未来会对应三条汇编语句
threadA做完第12步ticket变成999了准备执行第3步 但不幸的是threadA被切走了虽然被切走了但是等会回来还是从被切走的地方继续往下执行。寄存器只有一份但寄存器的内容属于当前进程的上下文threadA被切走了自然这些东西也被要拿走到threadA的上下文 threadB被调度 threadB是新来的它要重新开始执行这个语句。从1000开始减。第123步一直在疯狂执行。执行了800次当这一次ticket写回到内存变成200后再次执行到第一步 时间片到了就把threadB切换走了。 然后把thradA拿回来了首先恢复它的上下文它的寄存器放的依旧还是999然后继续执行第3步。 但是这一下就完蛋了threadB好不容易把ticket减到了200你一下给人感到了999。此时是不是相当于多线程运算的时候发生了干扰问题。
所以即便没有上面if只有- -依旧出现问题。
我们得到的结论就是我们定义的全局变量在没有保护的时候往往是不安全的像上面多个线程在交替执行造成的数据安全问题发生了数据不一致问题。
下一步就是解决这个问题。如何解决呢 加锁
在解释之前我们先复盘一下以前学过的知识。 1.多个执行流进行安全访问的共享资源 ---- 临界资源 2.我们把多个执行流中访问临界资源的代码 ---- 临界区 并不是整个代码都是临界区只有访问临界资源的代码才是临界区 临界区往往是线程代码中很小的一部分 3.想让多个线程串行访问共享资源 ---- 互斥 像刚刚的情况就是我们多线程在并发或并行的访问而没有保护而所导致出现的问题。 4.对一个资源进行访问的时候要么不做要么做完 ---- 原子性 理解它我们看看不是原子性的情况 就比如这个执行第12步但到第3步的是被切走了请问thradA对ticket- -操作是原子的吗 不是虽然它做了但是没做完有中间状态第一步第二步第三步是可以被打断了的这就不是原子性。虽然ticket- -只是一句语句但是未来会对应三条汇编语句。
我们给原子性一个的概念 一个对资源进行的操作如果只用一条汇编就能完成 ---- 原子性 反之就不是原子的 当前这样理解是为了方便表述所以这样理解。但是假设原子性是一个圈我们刚说的这个只是原子性的一个子集。
接下来我们细谈这把锁。 锁也是一个数据类型它的类型是
pthread_mutex_t如果一个锁定义好了我们必须对它进行初始化 如果是把局部锁就必须用init初始用完之后destroy销毁 mutex要初始化的锁 attr锁的属性我们设置成nullptr就可以了
全局的锁只需这样就自动初始化销毁 而使用锁想对某段代码进行安全访问必须加锁 未来不想保护了就解锁 我们用用这把锁
int ticket 10000;//全局锁
pthread_mutex_t lockPTHREAD_MUTEX_INITIALIZER;void *GetTicket(void *args)
{string name static_castconst char *(args);while (true){//加锁pthread_mutex_lock(lock);if (ticket 0){usleep(1000);cout name 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(lock);//解锁}else{//加锁可能条件不满足走到else这里也需要解锁pthread_mutex_unlock(lock);break;}//不能这里解锁不然if条件不满足走到else直接跳出循环还没有解锁//pthread_mutex_unlock(lock);}
}在加锁和解锁之间的就是传说中的临界资源而访问临界资源的代码就是临界区而通过加锁解锁也保证这部分代码要么不做要做完成。
局部锁我们也看看怎么用
int ticket 10000;//全局锁
//pthread_mutex_t lockPTHREAD_MUTEX_INITIALIZER;//又想把thread-1参数给GetTicket也想把锁给它
class ThreadData
{
public:ThreadData(pthread_mutex_t* mutex_p,const string name):_mutex_p(mutex_p),_threadname(name){}~ThreadData(){}
public:pthread_mutex_t* _mutex_p;string _threadname;
};void *GetTicket(void *args)
{ThreadData* tdstatic_castThreadData*(args);while (true){//加锁pthread_mutex_lock(td-_mutex_p);if (ticket 0){usleep(1000);cout td-_threadname 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td-_mutex_p);//解锁}else{//加锁可能条件不满足走到else这里也需要解锁pthread_mutex_unlock(td-_mutex_p);break;}}
}int main()
{//局部锁pthread_mutex_t lock;pthread_mutex_init(lock,nullptr);//使用前初始化
#define NUM 4vectorpthread_t tids(NUM);for (int i 0; i NUM; i){char namebuffer[64];snprintf(namebuffer, sizeof(namebuffer), thread-%d, i 1);//锁和threadname都传给函数ThreadData* tdnew ThreadData(lock,namebuffer);pthread_create(tids[i], nullptr, GetTicket, td);}for (auto thread : tids){pthread_join(thread, nullptr);}pthread_mutex_destroy(lock);//使用后销毁return 0;
}运行结果是我们的程序变慢了这是为什么?这里图片看不出来。 。并且只有一个线程在抢
加锁和解锁的过程是多个线程串行执行的所以程序变慢了。 只有一个线程在抢的原因是锁只规定互斥访问没有规定必须让谁先优先执行锁就是真是的让多个执行流进行竞争的结果。为什么一直是这个线程在跑因为它竞争锁的能力比其他线程强。
现在问题是一般抢完票就完了吗 当然不是我们所看到票的信息都是已经处理完之后给我们显示的。 所以一个线程抢完票之后还要做其他事情这样才有机会让其他线程有持有锁的可能性
void *GetTicket(void *args)
{//string name static_castconst char *(args);ThreadData* tdstatic_castThreadData*(args);while (true){//加锁pthread_mutex_lock(td-_mutex_p);if (ticket 0){usleep(1000);cout td-_threadname 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td-_mutex_p);//解锁}else{//加锁可能条件不满足走到else这里也需要解锁pthread_mutex_unlock(td-_mutex_p);break;}//模拟处理其他事情usleep(1);}
}接下来我们思考几个问题
1.如何看待锁
ticket是一个全局变量被多个线程同时访问我们称这个变量为共享资源或全局资源这个共享资源经过锁的保护变成了临界资源临界资源可以保证我们进行安全的访问这个没什么问题。 那这把锁是不是也一定是要被多个线程访问临界资源前要先访问这把锁。因为要保护共享资源所以要先加锁要加锁是不是得每个线程都先看到并访问这把锁。 a.锁本身就是一个共享资源 全局的变量是要被保护的而锁是用来保护全局的资源的。锁本身也是全局资源锁的安全谁来保护呢 b.pthread_mutex_lock、pthread_mutex_unlock加锁的过程必须是安全的 那是如何设计的呢 加锁的过程其实是原子的 要么就申请到要么就不申请不会存在中间状态。 c.如果申请成功就继续向后执行如何申请暂时没有成功执行流会如何
void *GetTicket(void *args)
{//string name static_castconst char *(args);ThreadData* tdstatic_castThreadData*(args);while (true){//加锁pthread_mutex_lock(td-_mutex_p);pthread_mutex_lock(td-_mutex_p);//申请一次再申请一次if (ticket 0){usleep(1000);cout td-_threadname 正在进行抢票: ticket endl;ticket--;//以微秒为单位进行休眠模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td-_mutex_p);//解锁}else{//加锁可能条件不满足走到else这里也需要解锁pthread_mutex_unlock(td-_mutex_p);break;}//模拟处理其他事情usleep(1);}
}我们看到代码就不再运行了多线程每一个线程都在但都不跑了 如果申请暂时没有成功执行流会阻塞(相当于挂起的状态) 去休眠了直到这个锁释放了OS或库会自动唤醒这个线程让它继续向后执行。
d.谁持有锁谁进入临界区
上面我们把临界资源临界区互斥都说过了现在也都能理解了可是原子性为了理解给上面的解锁只不过是一个子集。下面我们再把原子性问题谈一谈。 这个问题很好回答刚说的其他线程只能阻塞等待 绝对可以的随便切 当持有锁的线程被迫切走的时候是抱着锁被切走的即便自己被切走了其他线程依旧无法申请成功也便无法向后执行直到我最终释放这个锁 所以对于其他线程而言有意义锁的状态无非两种 1.申请锁前 2.释放锁后 而站在其他线程的角度看待当前线程持有锁的过程就是原子的
在未来我们使用锁的时候一定要尽量保证临界区的粒度(锁中间保护代码的多少)要非常小。因为是串行访问的。
并且如果让线程1线程2访问公共资源就加锁串行起来线程3不加锁直接访问这样是不对的。加锁是程序员行为必须做到要加就都要加
2.如何理解加锁和解锁的本质 加锁的过程是原子的 其实你注意会发现解锁的要求其实并不高我加锁了未来肯定只有一个执行流在解锁。解锁这件事情对原子性的要求或者安全性要求并不是特别特别强但我们依旧要保证它的原子性库也是这样设计的。
接下来我们就看看加锁解锁的实现原理是如何保证原子性的。
到现在我们已经发现单纯的 i 或者 i 都不是原子的有可能会有数据不一致性问题。 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在继续往下谈的时候我们先建立一个共识 1.CPU内寄存器只有一套被所有执行流共享 2.CPU内寄存器的内容是每个执行流私有的称之为运行时上下文。
线程A先跑要进行加锁加锁的伪代码就是lock下面的语句 线程A先把0放到%al寄存器中 此时线程A把0放到寄存器中可以被切走吗在加锁任何一条汇编执行前或执行后任何一条语句线程都可以被切换。 现在问题是把0放到%al中这条语句的本质是什么 是不是相当于把0值放到线程A的上下文当中 如果这个时候线程A被切走线程A一点都不担心因为它知道我要被切走时我要把这个0也要带走当我回来时我在把0放回来。
接下来执行下一句汇编 内存中的mutex变量线程A可以访问线程B也可以访问那这个mutex变量不就是我们的共享变量吗就如同ticket而刚刚竟然用一条汇编让线程A将寄存器中的值和内存中的值做交换这个交换的动作是一条汇编完成交换的本质是什么 交换的本质共享的数据交换到我的上下文中 就相当于线程A把这个锁拿走了此时刚把这条语句执行完线程A就被切走了切走了线程A怕不怕一点都不怕先别着急把我切走我要把我的上下文带走。 线程B被调度依旧要执行加锁的逻辑首先把0放到%al寄存器中 接下来也要进行交换可是现在交换是0换0线程B此时申请锁就不成功了
不成功线程B只能挂起等待了。 线程A只要申请成功了即使后序被切走了但是一点都不担心因为它是持有锁被切走的其他线程来也申请不到锁。 线程B被切走了把0也带走OS又调度线程然后恢复上下文把1放到%al里继续向下执行经过if判断申请锁成功返回执行自己的语句 解锁的代码就特别简单了一句mov把1拷贝到mutex唤醒其他等待锁的线程然后return就结束了 其他线程就可以以同样的逻辑进行加锁解锁。
3.如果我们想简单的使用该如何进行封装设计 把这个锁封装起来
//Mutex.hpp
#pragma once
#includeiostream
#includepthread.husing namespace std;class Mutex
{
public:Mutex(pthread_mutex_t* lock_pnullptr):_lock_p(lock_p){}void lock(){if(_lock_p) pthread_mutex_lock(_lock_p);}void unlock(){if(_lock_p) pthread_mutex_unlock(_lock_p);}~Mutex(){}private:pthread_mutex_t* _lock_p;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lockp):_mutex(lockp){_mutex.lock();//在构造函数中进行加锁}~LockGuard(){_mutex.unlock();//在析构函数中进行解锁}private:Mutex _mutex;
};//mythread.cc
#include unistd.h
#include vector
#include Mutex.hppint ticket 10000;// 又想把thread-1参数给GetTicket也想把锁给它
class ThreadData
{
public:ThreadData(pthread_mutex_t *mutex_p, const string name): _mutex_p(mutex_p), _threadname(name){}~ThreadData(){}public:pthread_mutex_t *_mutex_p;string _threadname;
};void *GetTicket(void *args)
{// string name static_castconst char *(args);ThreadData *td static_castThreadData *(args);while (true){{//构造的时候自动加锁后面的代码都处于加锁状态//局部变量生命周期随作用域也就是这个代码块//一次循环结束后自动调用析构函数也就是自动解锁了//这里不想把模拟其他事情也加锁不然都是一个线程再跑因此把加锁代码单独弄个作用域LockGuard lockguard(td-_mutex_p);if (ticket 0){usleep(1000);// cout name 正在进行抢票: ticket endl;cout td-_threadname 正在进行抢票: ticket endl;ticket--;}else{break;}}// 模拟处理其他事情usleep(1000);}
}int main()
{// 局部锁pthread_mutex_t lock;pthread_mutex_init(lock, nullptr); // 使用前初始化#define NUM 4vectorpthread_t tids(NUM);for (int i 0; i NUM; i){char namebuffer[64];snprintf(namebuffer, sizeof(namebuffer), thread-%d, i 1);ThreadData *td new ThreadData(lock, namebuffer);pthread_create(tids[i], nullptr, GetTicket, td);}for (auto thread : tids){pthread_join(thread, nullptr);}pthread_mutex_destroy(lock);return 0;
}就我们刚刚写的代码票都被一个线程抢了这个线程错了吗 它没错它本来就是互斥安全的访问但并不合理。造成了其他线程的饥饿状态在继续往下学习中我们先谈一点概念。
2.可重入VS线程安全 线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。 重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数函数状态随着被调用状态发生变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数因为malloc函数是用全局链表来管理堆的调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构可重入函数体内使用了静态的数据结构
常见可重入的情况:
不使用全局变量或静态变量不使用用malloc或者new开辟出的空间不调用不可重入函数不返回静态或全局数据所有数据都有函数的调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的那就是线程安全的函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种(线程一般是要调函数的构造析构等等)线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
3.常见锁的概念
这里我们就说一种 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
具体来说就是指一组执行流不管是线程还是进程它在持有自己的锁资源的同时还想方设法的去申请对方的锁资源因为大家互相持有自己的还申请对方的而锁是不可抢占的锁不可抢占就是我拿了锁除非我主动归还否则你想要我的锁我不给。所以当我们互相持有自己的锁还想要对方的锁的时候进而可能导致我们多执行流互相等待对方的资源而导致代码无法推进的情况。
举个例子 张三和李四这两个小朋友一起去商店我是这家店的老板他俩爸爸妈妈各自给他们一人五毛钱问我棒棒糖多少钱我说一块钱张三问李四是不是有五毛钱李四说是张三说你把五毛钱给我把我凑成一块钱买棒棒糖好不好李四说为什么我要给你你怎么不给我呢你能不能把你的五毛钱给我我凑成一块钱我来买棒棒糖。张三当然也不同意了。 这两个小朋友就互相拿着自己五毛钱还在要着对方的五毛钱两个人争执不下谁都没买成棒棒糖这两个小朋友的状态就叫做死锁。
接下来我们谈谈死锁。 在多把锁的场景下我们持有自己的锁不释放还要对方的锁对方也是如此此时就容易造成死锁
1.一把锁有可能死锁吗 当然有可能。 就比如说别人可以把你绊倒你自己可以把自己绊倒吗当然是可以的。 2.为什么会有死锁我们看看逻辑链条
一定是你用了锁—为什么你要用锁呢—保证临界资源的安全----为什么要保证临界资源的安全?—多线程访问我们可能会出现数据不一致的问题—为什么会出现数据不一致的问题?—因为是我们多线程并且使用的是全局资源—为什么多线程访问全局资源会造成这样的问题—多线程大部分资源(全局的)是共享的—多线程共性
任何技术都有自己的边界是解决问题的但是可能在解决问题的同时一定可能会引入新的问题
死锁四个必要条件
互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
这是锁的四个必要条件可能还会有其他条件但是只要你需要得到死锁这四个条件必须同时满足。
互斥很好理解就是必须保证访问某种资源是互斥的这个没什么问题这个是我们锁的基本特性你是一把锁本身就具有互斥能力没有互斥能力怎么能谈你是一把锁呢
请求与保持请求就是我要你保持就是我不释放我的。我要你的但我不释放我的。
不剥夺就像刚才要五毛钱你要不给我我就揍你这是剥夺另一种是把五毛钱给我你别害怕我不打你我也不抢你的我要你自愿给我。这叫做不剥夺条件。
循环等待A有自己的锁它去要B的锁B有自己的锁它去要A的锁。这就是循环等待刚才的张三和李四就是形成了循环等待。
一旦死锁这四个条件都必须同时满足那只要破坏这四个条件之间的其中一个死锁就不满足了。
避免死锁
破坏死锁的四个必要条件
互斥是锁的一种特性这不用考虑了没有办法破坏你不用锁了也根本不会产生死锁问题。
请求与保持好处理比如说我们的线程要访问一个或多个临界资源它需要同时拥有两把锁申请第一把锁成功如果第二把锁申请失败了失败了就把自己曾经申请的锁释放掉。此时就不会造成死锁了。
不剥夺所谓不剥夺就是不能抢那我们可以设置一个竞争策略比如A申请到锁再去申请B的锁B的锁被其他线程拿到了这个时候我们比较定义的优先级或其他假设A的优先级比较高那拿到B的锁的线程必须主动释放锁。
循环等待就相当于我们在申请锁的时候A线程先申请A锁在申请B锁而B线程先申请B锁在申请A锁所以两个线程天然申请锁的顺序就是环状的。我们可以尽量不让线程出现这个环路情况我们让两个线程申请锁的顺序保持一致就可以破坏循环等待问题。两个线程都是先申请A锁在申请B锁。
加锁顺序一致避免锁未释放的场景资源一次性分配
资源一次性分配比如说你有一万行代码有五处要申请锁你可以最开始一次就给线程分配好而不要把五处申请打散到代码各个区域里所导致加锁场景非常复杂
避免死锁算法
死锁检测算法(了解)银行家算法了解
一个线程申请到锁另一个线程可以解锁吗比如说A线程申请到锁B线程可以释放这个锁吗 注意这一句可是把1拷贝到内存种mutex可不是把线程上下文中的1拷贝过去。 所以是可以的