【c++与Linux进阶】线程篇 -互斥锁
1. 前言在我们之前学习的代码种就是在建造多线程的路上我们可以看到出现了乱码或者抢占输出这是为什么呢本章将带着这个问题来带你思考一个例子先来领略问题的所在。什么是线程互斥.见识互斥锁。使用互斥锁2. 一个买票的例子假设我们有100张电影票我们同时抢票会出现什么我们来尝试写代码来看看#includeiostream#includethread#includevector#includestring#includecstdio#includeunistd.hintticket100;voidroutine(std::string name){while(1){if(ticket0){usleep(1000);// 说明可以开始抢票ticket--;printf(%s shell ticket,now tickets number:%d\n,name.c_str(),ticket);}else{std::coutticketstd::endl;break;}}return;}intmain(){std::vectorstd::threadthreads;for(inti0;i5;i){std::string namethread-;namestd::to_string(i);threads.emplace_back(routine,name);}for(autothread:threads){thread.join();}return0;}这里的公共的资源是ticket很显然是五个线程去抢这个票数其中我们用usleep1000来表示抢票消耗的时间。按照常理来说我们一旦没票了就应该停止。让我们运行来看看结果会运行到 -4我的票都没有了这为什么会运行成为这样嘞2-1 原因如果是单线程来说是不会发生这件事的但是这里是多线程多线程很大的一个特点就是竞争。我们来看我们的代码我们每个线程进入这个函数都会拿到ticket的数量。随后休息一秒钟在进行对其减减。我们放慢过程详细的来看看当票数为1的时候的情况我们可以假设线程1拿到ticket票之后发现是1随后休息1秒随后线程2启动发现这个ticket也是1也是可以进行减减。其中线程2也会休息一秒。我们线程1在拿入ticket在进行减减导致变成0。关键的来了由于线程2之前做过了判断。可以进行减减我们在对ticket进行减减就导致变成了-1。为什么是这样的过程我来大致写写; if (ticket 0) LOAD R1, [ticket] ; R1 ticket CMP R1, 0 ; 比较 R1 和 0 JLE END_IF ; 如果 0跳走 ; usleep(1000) CALL usleep ; ticket-- LOAD R2, [ticket] ; R2 当前 ticket SUB R2, 1 ; R2 R2 - 1 STORE [ticket], R2 ; 写回 ticket END_IF:注意这里最重要的一点判断时用的是R1真正减法时又重新LOAD R2, [ticket]读了一次内存我上面写的可能有歧义但是我们在联系汇编来详细的讲讲线程1启动发现ticket是1可以进行减减执行usleep1000。注意这里比较分三步进入寄存器比较从寄存器种写回。线程2启动发现ticket是1可以进行减减执行usleep1000。关键的来了线程1对其减减。这个减减是进入寄存器对进行减减在写回ticket这个ticket已经发生改变了那么线程2拿到的ticket就是已经被线程1改变的ticket了线程2对之后写回的ticket进行减减这个就是经典的check-then-act race。3. 引入锁的概念为了防止上面的乌龙的事件我们引入了锁的概念先不说是什么我们先来看看他的威力#includeiostream#includethread#includevector#includestring#includecstdio#includeunistd.hintticket100;pthread_mutex_t lockPTHREAD_MUTEX_INITIALIZER;voidroutine(std::string name){while(1){pthread_mutex_lock(lock);if(ticket0){usleep(1000);//说明可以开始抢票ticket--;printf(%s shell ticket,now tickets number:%d\n,name.c_str(),ticket);pthread_mutex_unlock(lock);}else{pthread_mutex_unlock(lock);break;}}return;}intmain(){std::vectorstd::threadthreads;for(inti0;i5;i){std::string namethread-;namestd::to_string(i);threads.emplace_back(routine,name);}for(autothread:threads){thread.join();}return0;}我们来看看代码运行的情况我们可以看到这个是没有问题的的确完成了检票的任务。那么这里的锁是什么互斥锁 (Mutex)特点“互斥”即其名同一时间只有一个线程能持有锁。用法pthread_mutex_lock()加锁pthread_mutex_unlock()解锁。这里就是全局锁的初始化和上锁和解锁。3-1互斥锁上锁的位置我们先来回忆为什么需要上锁是不是由于线程出现竞争导致公共资源出现混乱所以一切访问公共资源的地方都需要上锁一次只允许一个线程去访问使用。那么我讲的这些就是临界区的概念 什么是临界区 (Critical Section)临界区是指代码中访问共享资源如全局变量、外部文件、共享内存等的那一部分程序段。核心规则同一时刻只允许一个线程进入临界区。如果不保护就会发生“竞态条件”Race Condition导致数据毁坏。保护方式进入临界区前加锁Lock离开临界区后解锁Unlock。3-2 解锁的时机我们可以看到我的代码无论是在if还是else我们都会解锁就是解除锁有人就说了为什么不像后面这个代码一样直接解除锁呢pthread_mutex_lock(lock);if(ticket0){//说明可以开始抢票ticket--;printf(%s shell ticket,now tickets number:%d\n,name.c_str(),ticket);//pthread_mutex_unlock(lock);}else{//pthread_mutex_unlock(lock);break;}pthread_mutex_unlock(lock);那么else就永远不会解锁他直接break这就会导致出现另一个问题这正是一种典型的死锁诱因一个线程在持有锁的情况下直接退出如 break、return 或异常而未释放锁导致其他需要该锁的线程永远等待。死锁产生的四个必要条件Coffman 条件互斥Mutual Exclusion资源只能被一个线程独占。占有并等待Hold and Wait线程已持有至少一个资源并等待获取其他资源。不可剥夺No Preemption资源只能由持有者主动释放。循环等待Circular Wait线程之间形成一条循环等待资源链。3-3 线程拿着锁睡觉这是我们这个代码的另一个问题我们里面的usleep应该删除掉避免锁拿着线程进行睡觉这是非常不合理的所以综合下来我们的程序应该是这样的#includeiostream#includethread#includevector#includestring#includecstdio#includeunistd.hintticket100;pthread_mutex_t lockPTHREAD_MUTEX_INITIALIZER;voidroutine(std::string name){while(1){pthread_mutex_lock(lock);if(ticket0){//说明可以开始抢票ticket--;printf(%s shell ticket,now tickets number:%d\n,name.c_str(),ticket);pthread_mutex_unlock(lock);}else{pthread_mutex_unlock(lock);break;}}return;}intmain(){std::vectorstd::threadthreads;for(inti0;i5;i){std::string namethread-;namestd::to_string(i);threads.emplace_back(routine,name);}for(autothread:threads){thread.join();}return0;}3-4 一个现象我们发现一直是线程2在进行抢票这一段里一直是thread-2在卖票说明这段时间里它反复拿到了 CPU并且每次也都先抢到了那把锁。它先抢到 CPU于是更有机会再次执行到pthread_mutex_lock而锁一旦被它释放它又很快再次抢回来了。所以互斥锁并不能保证公平。4 总结这篇文章从一张神奇的负数车票开始带我们走进了多线程编程中最头疼的问题——竞态条件。当我们用五个线程同时去抢那100张票时本该在票数为0时就停止的程序竟然一路狂奔到了-4。这背后的元凶就是经典的check-then-act race线程A刚判断完票数大于0还没完成减减操作就被线程B抢占了CPU等A回来继续执行时手里的旧情报已经失效了却还要对已经变了的票数再做一次减减。这种对公共资源的并发访问如果不加以保护数据就会像脱缰的野马一样乱套。为了解决这个问题我们引入了互斥锁Mutex这个交通警察。它保证同一时间只有一个线程能进入临界区——也就是访问共享资源的那段代码。加锁和解锁的时机很有讲究锁的范围要刚好覆盖对公共资源的操作但不能太大比如不能把usleep也包进去否则就是拿着锁睡觉白白浪费别人的时间同时每一个分支路径都要记得解锁不然就会触发死锁让其他线程永远等在那里。文章最后也提了一个有趣的现象即便有了锁线程2还是能把票抢光——这说明互斥锁只保证互斥不保证公平谁抢到CPU谁就有机会先拿到锁。总的来说线程互斥是多线程编程的必修课。理解临界区、掌握锁的粒度、警惕死锁的四个必要条件这些基本功打扎实了才能写出既高效又安全的多线程程序。毕竟在这个并发为王的时代让线程们有序竞争比野蛮抢食要靠谱得多。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2410614.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!