南充网站网站建设,顺德网络科技有限公司,廊坊头条新闻最新消息新闻,中国企业信用网站官网1 #x1f351;线程间的互斥相关背景概念#x1f351;
先来看看一些基本概念#xff1a;
1️⃣临界资源#xff1a;多线程执行流共享的资源就叫做临界资源。2️⃣临界区#xff1a;每个线程内部#xff0c;访问临界资源的代码#xff0c;就叫做临界区。3️⃣互斥…1 线程间的互斥相关背景概念
先来看看一些基本概念
1️⃣临界资源多线程执行流共享的资源就叫做临界资源。2️⃣临界区每个线程内部访问临界资源的代码就叫做临界区。3️⃣互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用原子性后面讨论如何实现不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成。
互斥量mutex
大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个线程其他线程无法获得这种变量。但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。多个线程并发的操作共享变量(比如全局变量)会带来一些问题。
比如一个大家熟知的栗子售票。我们用一个全局整形变量记录票的个数多个线程并发的去抢票我们不难写出下面这样的代码
int g_ticket10000;void* Run(void* args)
{string namestatic_castconst char*(args);while(true){if(g_ticket0){break;}else{coutI am name,is running ticketsg_ticketendl;g_ticket--;}usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i0;i5;i){char* namenew char[26];snprintf(name,26,pthread%d,i1);pthread_create(ptidsi,nullptr,Run,name);}for(int i0;i5;i){pthread_join(ptids[i],nullptr);}return 0;
}当我们运行时 我们发现有多个线程抢到了同一张票并且打印混乱。有些情况下票还有可能变成了负数而这就是线程不安全所带来的问题解决办法我们在下面会给出详细解释。 2 用互斥锁解决线程安全问题
2.1 分析问题
我们来分析下上面的代码为什么会出现那样的结果
if 语句判断条件为真以后代码可以并发的切换到其他线程。usleep 这个模拟漫长业务的过程在这个漫长的业务过程中可能有很多个线程会进入该代码段。减减ticket 操作本身就不是一个原子操作。
我们可以取出渐渐ticket取出ticket–部分的汇编代码:
objdump -d a.out test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 ticket
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 ticket- - 操作并不是原子操作而是对应三条汇编指令
load 将共享变量ticket从内存加载到寄存器中;update : 更新寄存器里面的值执行-1操作;store 将新值从寄存器写回共享变量ticket的内存地址。
要解决以上问题需要做到三点
1️⃣代码必须要有互斥行为当代码进入临界区执行时不允许其他线程进入该临界区。2️⃣如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行那么只能允许一个线程进入该临界区。3️⃣如果线程不在临界区中执行那么该线程不能阻止其他线程进入临界区。
要做到这三点本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
2.2 互斥量的接口
初始化互斥量
初始互斥量有两种方式
方法1静态分配:
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;方法2动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数
mutex要初始化的互斥量
attrNULL这两种方式选择哪一种都是OK的。
销毁互斥量
销毁互斥量需要注意
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁;不要销毁一个已经加锁的互斥量;已经销毁的互斥量要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex)互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号调用pthread_mutex_lock 时可能会遇到以下情况:
互斥量处于未锁状态该函数会将互斥量锁定同时返回成功。发起函数调用时其他线程已经锁定互斥量或者存在其他线程同时申请互斥量但没有竞争到互斥量那么pthread_ lock调用会陷入阻塞(执行流被挂起)等待互斥量解锁。
所以我们可以改进下上面的抢票
int g_tictet10000;
pthread_mutex_t mtuPTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void* Run(void* args)
{string namestatic_castconst char*(args);while(true){pthread_mutex_lock(mtu);if(g_tictet0){pthread_mutex_unlock(mtu);break;}else{coutI am name,is running ticketsg_tictetendl;g_tictet--;}pthread_mutex_unlock(mtu);usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i0;i5;i){char* namenew char[26];snprintf(name,26,pthread%d,i1);pthread_create(ptidsi,nullptr,Run,name);}for(int i0;i5;i){pthread_join(ptids[i],nullptr);}return 0;
}当我们再次运行时 我们发现不会出现多个线程抢占同一张票并且打印混乱的情况了。
代码中值得注意的事情有加锁的策略是选用的粒度一般是越细越好。
互斥量实现原理探究
搞了这么多那么互斥量的实现原理究竟是啥捏
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
我们可以自己实现一份lock和unlock的伪代码
lock:movb $0,%alxchgb %al,mutexif(al寄存器的内容0)return 0;//表示申请锁成功else挂起等待;goto lock;unlock:movb $1,%al唤醒等待mutex的线程;return 0;//表示释放锁成功
通过上面的伪代码我们可以知道当初始值mutex的值为1时假设线程1先进行申请锁会先将寄存器中的值改为0然后用寄存器中的0交换mutex中的1此时1被线程1给拿到了假设此时线程1的时间片到了要切换线程2执行在切换之前先保存了线程1的上下文数据然后切换此时线程2从头执行将寄存器中的数值改为0然后交换但是唯一的1已经被线程1给拿走了所以线程而只有挂起等待当重新切换回线程1的时候线程1会重新恢复上下文数据也就是寄存器的内容会被恢复到切换前所以判断寄存器的内容0申请成功。此时我们发现就算是有多个线程并发的抢占锁资源时也只有一个线程能够申请成功其他线程在挂起等待因为这里面的1只有一个并且是以交换形式进行的可以理解这里面的1本质就是一把锁。
释放资源就更好理解了将寄存器的值修改为1然后唤醒等待锁的线程即可。从释放锁的那段伪代码中我们也能够看到当多个线程申请同一把锁时一个线程申请了锁后虽然其他线程不能够申请了但是却可以释放该锁。 2.3 可重入VS线程安全
概念
线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。
常见的线程不安全/安全的情况
不安全情况
1️⃣不保护共享变量的函数2️⃣函数状态随着被调用状态发生变化的函数3️⃣返回指向静态变量指针的函数4️⃣调用线程不安全函数的函数
安全情况
1️⃣每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的2️⃣类或者接口对于线程来说都是原子操作3️⃣多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入/可重入的情况
不可重入
1️⃣调用了malloc/free函数因为malloc函数是用全局链表来管理堆的2️⃣调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构3️⃣可重入函数体内使用了静态的数据结构
可重入
1️⃣不使用全局变量或静态变量2️⃣不使用用malloc或者new开辟出的空间3️⃣不调用不可重入函数4️⃣不返回静态或全局数据所有数据都有函数的调用者提供5️⃣使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系与区别
联系
1️⃣函数是可重入的那就是线程安全的2️⃣函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题3️⃣如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
区别
1️⃣可重入函数是线程安全函数的一种2️⃣线程安全不一定是可重入的而可重入函数则一定是线程安全的。3️⃣如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
2.4 死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。 死锁四个必要条件
互斥条件一个资源每次只能被一个执行流使用。请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放。不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺。循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁
破坏死锁的四个必要条件加锁顺序一致避免锁未释放的场景资源一次性分配
死锁避免算法有银行家算法和死锁检测算法大家有兴趣可以自行下去研究。 3 用封装使代码更加优雅
我们上面写的代码中我们能否自己实现一个简易版本的创建线程(类似于C11提供的线程库那样)的类呢以及加锁和解锁能够使用RAII的思想来帮助我们完成呢当然是可以的我们可以自己实现一个更加优雅的代码
mutexGuard.hpp:
#pragma once
#includeiostream
#includepthread.h
using namespace std;class mutexGurad
{
public:mutexGurad(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~mutexGurad(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};
thread.hpp:
#pragma once
#include iostream
#include functional
using namespace std;class threadProcess
{
public:enum stu{NEW,RUNNING,EXIT};template class TthreadProcess(int num, T exe, void *args): _tid(0),_status(NEW),_exe(exe),_args(args){char name[26];snprintf(name, 26, thread%d, num);_name name;}static void *runHelper(void *args){threadProcess *ts (threadProcess *)args; (*ts)();return nullptr;}void operator()() // 仿函数{if (_exe ! nullptr)_exe(_args);}void Run(){int n pthread_create(_tid, nullptr, runHelper, this);if (n ! 0)exit(-1);_status RUNNING;}void Join(){int n pthread_join(_tid, nullptr);if (n ! 0)exit(-1);_status EXIT;}private:string _name;pthread_t _tid;stu _status;functionvoid *(void *) _exe;void *_args;
};测试程序
int g_tictet 10000;
pthread_mutex_t mtu PTHREAD_MUTEX_INITIALIZER;void *Run(void *args)
{string name static_castconst char *(args);while (true){{mutexGurad mutGuard(mtu);if (g_tictet 0){break;}else{cout I am name ,is running tickets g_tictet endl;g_tictet--;}}usleep(1000);}return nullptr;
}int main()
{threadProcess thpro1(1, Run, (void *)thread1);threadProcess thpro2(2, Run, (void *)thread2);threadProcess thpro3(3, Run, (void *)thread3);thpro1.Run();thpro2.Run();thpro3.Run();thpro1.Join();thpro2.Join();thpro3.Join();return 0;
}当我们运行时 我们依旧能够得到正确的结果并且代码写起来也好看多了。除此之外我们还可以拿到线程的其他特性这里我就不在测试了。