辽源商城网站建设,网站建设结课总结,优秀网页设计导航,备案个人网站做淘宝客Linux之封装线程库和线程的互斥与同步 一.封装线程库二.线程的互斥2.1互斥量的概念2.2初始化和销毁互斥量2.3加锁和解锁2.4互斥量的原理2.5可重入和线程安全2.6死锁 一.封装线程库
其实在我们C内部也有一个线程库而C中的线程库也是封装的原生线程库的函数#xff0c;所以我们… Linux之封装线程库和线程的互斥与同步 一.封装线程库二.线程的互斥2.1互斥量的概念2.2初始化和销毁互斥量2.3加锁和解锁2.4互斥量的原理2.5可重入和线程安全2.6死锁 一.封装线程库
其实在我们C内部也有一个线程库而C中的线程库也是封装的原生线程库的函数所以我们也可以自己来封装一个自己的线程库。
//mythread.hpp
#pragma once #include iostream
#include pthread.h
#include string
#include functional
#include unistd.htemplateclass T
using func_t std::functionvoid(T);templateclass T
class Thread
{
public:Thread(const std::string ThreadName,func_tT func,T date):_ThreadName(ThreadName),_func(func),_date(date),_tid(0),_isrunning(false){}~Thread(){}//注意这是类内成员函数所以它不止是有void* args这个参数//还有一个this指针当作参数所以就会造成pthread_create调用出错//因为pthread_create函数第三个参数是一个返回值为void*参数为void*的函数指针//所以想要解决这个问题我们可以利用static关键字将其变为全局函数//并且再将this指针当作参数传给它这样也就可以解决全局函数无法访问私有成员变量的问题static void* ThreadRoutine(void* args){Thread* ts static_castThread*(args);ts-_func(ts-_date);return nullptr;}bool Start(){int n pthread_create(_tid,nullptr,ThreadRoutine,this);if(n 0){_isrunning true;return true;} else{return false;}}bool Join(){if(!_isrunning){return true;}int n pthread_join(_tid,nullptr);if(n 0){_isrunning false;return true;}return false;}std::string Theadname(){return _ThreadName;}bool IsRunning(){return _isrunning;}private:pthread_t _tid;std::string _ThreadName;bool _isrunning;func_tT _func;T _date;
};//mythread.cc
#include mythread.hppstd::string GetThreadName()
{static int number 1;char name[64];snprintf(name,sizeof(name),Thread-%d,number);return name;
}void Print(int num)
{while(num){std::cout i am a new thread num-- std::endl;sleep(1);}
}int main()
{Threadint t(GetThreadName(),Print,10);t.Start();t.Join();return 0;
}二.线程的互斥
2.1互斥量的概念
在聊线程的互斥之前我们可以通过一个实验来观察到我们多线程代码的一个问题
// mythread.cc
#include mythread.hppstd::string GetThreadName()
{static int number 1;char name[64];snprintf(name, sizeof(name), Thread-%d, number);return name;
}void Print(int num)
{while (num){std::cout i am a new thread num-- std::endl;sleep(1);}
}class ThreadDate
{
public:ThreadDate(const std::string threadname): _threadname(threadname){}std::string _threadname;
};//抢票程序
int ticket 10000;
int GetTicket(ThreadDate* td)
{while (true){if (ticket 0){//充当抢票的时间usleep(1000);std::cout td-_threadname.c_str() get a ticket: ticket std::endl;ticket--;}else{break;}}
}int main()
{std::string name1 GetThreadName();ThreadDate* td1 new ThreadDate(name1);ThreadThreadDate* t1(name1,GetTicket,td1);std::string name2 GetThreadName();ThreadDate* td2 new ThreadDate(name2);ThreadThreadDate* t2(name2,GetTicket,td2);std::string name3 GetThreadName();ThreadDate* td3 new ThreadDate(name3);ThreadThreadDate* t3(name3,GetTicket,td3);std::string name4 GetThreadName();ThreadDate* td4 new ThreadDate(name4);ThreadThreadDate* t4(name4,GetTicket,td4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();delete td1;delete td2;delete td3;delete td4;return 0;
}正常来说我们运行这个抢票程序最后打印的应该是抢到1号票后面就不会再继续抢票了那么事实真的像我们想的这样吗 我们发现并不像我们所说的那样线程居然会抢到0号-1号-2号票这根本不符合我们的代码逻辑啊这是为什么呢
这就要提到我们之前学习进程间通信的知识了 在学习进程间通信的时候我们提到过公共资源的概念而公共资源是需要保护的这是因为如果多个执行流去访问公共资源很容易产生数据不一致的情况并且数据不一致现象的出现本质上来说也是因为访问公共资源的程序不是原子性的所以公共资源是需要被保护的。那么被保护起来的公共资源就是我们说的临界资源而访问临界资源的代码就是临界区保护的手段当时我们也提了一下分为互斥和同步。互斥简单来说就是让任何时刻只允许一个执行流去访问公共资源而同步则是让多个执行流保持着一种顺序去访问公共资源。 那么我们可以思考一下了我们刚刚定义的全局变量ticket是否也算是一种公共资源呢我们模拟的抢票程序不就是在访问公共资源吗而我们并没有对ticket进行保护它不就会出现数据不一致的情况吗 在知道了这种情况产生的原因后我们来简单阐述一下产生的过程 在Linux中我们不太好观察汇编语句我们可以用vs来观察一下。 我们老是说因为多线程访问公共资源的操作不是原子的从而导致数据不一致的情况产生但是从来没说这个情况到底是怎么产生的其中的过程到底是什么样的。接下来我结合图和文字来为大家解释。
首先我用文字来为大家描述一下数据不一致的整个过程。 假设有两个线程A和B并且此时ticket已经为1了线程A先执行当它在进行第一步即读取ticket的值1传到自己的寄存器后A的时间片到了CPU切换到了线程B而A就带着自己寄存器的内容即它的上下文去等待下一次的调度。现在线程B来执行这五步操作执行的非常顺利它成功的抢到了最后一张票并将内存中ticket的减为了0。然后CPU切换回了线程A让其继续执行这五步所以线程A将自己的上下文特别是ticket的值1写入到了寄存器中随后继续执行未完成的第二步即让CPU来判断寄存器中存储的ticket是否大于0判断结果是大于0的。 注意此时的线程A的判断结果其实是错误的因为ticket的已经被线程B减到0了但是由于线程A在切换之前已经将ticket的值读取到了自己的寄存器中而CPU进行判断又是判断的是寄存器中的内容而不是此时内存中的内容。这就导致了线程A判断结果是大于0的。 在判断结果是大于0后线程A继续执行剩下的三步先将内存中ticket的0读取到寄存器中再进行减一最后写入到内存中从而导致了线程A抢到了本不应该存在的第0号票 而这只是两个线程并发访问的情况要知道我们刚刚可是创建了四个线程来抢票所以更容易产生这样的问题即可能有三个线程都是ticket1的时候读取了ticket的值到寄存器中随后在要进行第二步的时候被切换走了之后第四个线程完成了–的操作让ticket为0。但是那三个线程在被切换回来后还是继续执行了剩下的四步导致抢到了第0号第-1号第-2号。
那么要如何解决这个问题呢如果我们只允许一个线程进入临界区去访问临界资源不就不会产生这种问题了吗这种解决办法用专业一点的术语就是加锁也就是利用互斥。而这把锁也就叫做互斥量。 那么接下来我们来介绍一下加锁的一些接口先了解接口我们再使用并且理解。
2.2初始化和销毁互斥量
初始化互斥量有两种方法分为静态初始化互斥量和动态初始化互斥量
静态初始化互斥量 非常的简单我们可以在任意地方静态初始化互斥量并且静态的互斥量不需要销毁动态初始化和销毁互斥量
动态初始化互斥量中第一个参数是一个互斥量变量第二个参数是互斥量的属性通常我们设为NULL即可。 销毁互斥量只需要传入一个互斥量变量即可。 注意不要销毁一个被加锁的互斥量并且已经被销毁的互斥量之后要确保没有线程再对其进行加锁 2.3加锁和解锁 第一个函数就是加锁第二个函数是检测是否能加锁第三个函数是解锁 在加锁的时候可能会遇到两种情况
这个互斥量没有被加锁那么调用lock函数就会加锁成功并且返回0这个互斥量已经被加锁或者和其他线程同时申请这个互斥量时没有竞争过其他线程那么调用lock函数就会加锁失败并且会将次线程移入到阻塞队列中进行等待直到互斥量解锁。 而如果不想让线程在申请加锁失败的情况下被阻塞的话就可以使用第二个函trylock使用这个函数加锁失败时只会返回错误码。
那么我们来尝试使用一下互斥量吧 2.4互斥量的原理
可是就如同我们面对信号量时的问题又出来了这么多的线程去访问互斥量那它不就变成一个公共资源了那它不也需要被保护吗但是它肯定不能再去让别人保护它吧所以对于互斥量的保护只能从它自己入手也就是让它对加锁的操作变成原子的。这又是如何做到的呢 我们也可以用图来解释 想要理解这部分的内容需要大家知道几个知识 寄存器在硬件中只有一份但是寄存器的内容是具有多份的这就导致每个线程都有一个属于自己的上下文。 而xchgb的作用就是将一个共享的mutex资源交换到一个线程的自己的上下文中从而让其属于线程自己进而完成加锁的工作
2.5可重入和线程安全
在之前学习进程间通信的时候我们谈到过可重入函数的概念如今我们学会了并发多线程访问公共资源就更需要关注可重入的问题了 我们先来复习一下可重入函数的概念 当同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。 同时我们要注意可与不可重入函数是不分褒贬不分好坏的可不可以重入只是函数的一个特点而已
在我们学习了线程后肯定要谈论到线程的安全问题 对于线程的不安全问题一般是有以下几个情况
不保护共享变量的函数函数状态随着被调用状态发生变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数
那么线程的不安全问题和重入函数又有什么关系呢 我们一般来说函数是可重入那么就是线程安全的如果函数不可重入那就不可以让多线程去使用不然就会引发线程的安全问题。并且如果一个函数中全局变量那么整个函数既不是可重入的也不是线程安全的。
而线程安全又和可重入函数有什么区别呢为什么要把这两个概念放在一起讲述 可重入函数是线程安全函数的一种 线程安全不一定是可重入的但是可重入函数一定是线程安全的。 不可重入函数不一定是线程不安全的线程不安全也不一定是不可重入函数。 线程安全与否是线程的一种特征而函数是否可重入则是函数的一种特点。
2.6死锁
在我们了解了锁后我们现在来思考使用锁是否会带来一些问题呢 在我们刚刚使用锁后很明显的问题的就是代码的执行速度变慢了这也很正常因为本来是多线程去访问临界资源的使用锁后变成了只能一个一个的去访问临界资源速度当然会变慢。
但是锁的问题不仅仅出现在这里我们来假设一个场景 在这个场景中有两个线程有两把锁A线程拥有A锁B线程拥有B锁但是于此同时呢A线程又去申请B锁B线程又去申请A锁这件造成这两个线程全部都申请失败并且进入阻塞状态从而导致整个临界区卡住了因为两个执行流全部都阻塞住了他们俩都不愿意放下自己的锁并且还想申请别的锁。这种问题就是死锁问题。 所以死锁是指在一组进程中的各个线程均占有不会释放的资源但因互相申请被其他线程所占用不会释放的资源而处于的一种永久等待状态。 我们上面演示的只是死锁的一种出现情况而已其实对于死锁来说有着四个必要条件
互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
这四个必要条件我们可以继续用上面的例子来进行解释 只要你用了锁就满足了互斥条件。 像A线程那样有了A锁又去申请B锁同时还不解开A锁就满足了请求与保持条件。 A线程去申请B锁就必须在B解开锁后才能申请成功没法直接强取豪夺了就满足了不剥夺条件。 A线程申请B锁B线程申请A锁导致A线程在等待B线程解开B锁而B线程又在等待A线程解开A锁从而满足循环等待条件。
那么想要避免死锁问题就只要破坏上面四个必要条件中的一个或者多个即可 如破坏互斥条件就只需要不用锁就行破坏请求与保持条件就让一个线程在申请锁资源前把自己拥有的锁全部解开破坏不剥夺条件也是类似请求与保持条件破坏循环等待条件就是让一个线程按照顺序去申请全部的锁资源就可以。这也就是为什么多线程代码的编写难度要难的一个原因。