当前位置: 首页 > news >正文

低价网站建设怎么样陕西营销型手机网站建设

低价网站建设怎么样,陕西营销型手机网站建设,平凉建设局网站,设计自己的名字图画人生总是那么痛苦吗#xff1f;还是只有小时候是这样#xff1f; —总是如此 文章目录 一、线程互斥1.多线程共享资源访问的不安全问题2.提出解决方案#xff1a;加锁#xff08;局部和静态锁的两种初始化/销毁方案#xff09;2.1 对于锁的初步理解和实现2.2 局部和全局锁…人生总是那么痛苦吗还是只有小时候是这样 —总是如此 文章目录 一、线程互斥1.多线程共享资源访问的不安全问题2.提出解决方案加锁局部和静态锁的两种初始化/销毁方案2.1 对于锁的初步理解和实现2.2 局部和全局锁的两种加锁方案的代码实现 3.根据代码现象提出问题3.1 如何看待锁3.2 如何理解加锁和解锁的本质硬件层面和软件层面的加锁3.3 RAII风格的封装设计锁构造函数加锁析构函数解锁 4.可重入与线程安全5.死锁5.1 死锁概念5.2 产生死锁的四个必要条件 二、线程同步生产消费模型1.通过条件变量抛出线程同步的话题2.生产消费模型的概念理解321原则3.条件变量实现线程同步的原理条件变量内部维护了线程的等待队列能wait线程也能wakeup线程4.串行、并发、并行的概念5.条件变量的基本代码编写 三、基于blockqueue的生产消费模型1.双阻塞队列的多生产多消费模型的实现2.生产消费模型高效在哪里不影响其他多线程并发或并行的获取任务和执行任务 一、线程互斥 1.多线程共享资源访问的不安全问题 1. 假设现在有一份共享资源tickets如果我们想让多个线程都对这个资源进行操作也就是tickets- -的操作但下面两份代码分别出现了不同的结果上面代码并没有出现问题而下面代码却出现了票为负数的情况这是怎么回事呢 其实问题产生就是由于多线程被调度器调度的特性导致的。 2. 了解上面的问题需要知道线程调度的特性实际线程在被调度时他的上下文会被加载到CPU的寄存器中而线程在被切换的时候线程又会带着自己的上下文被切换下去此时要进行线程的上下文保存以便于下次该线程被切换上来的时候能够进行上下文数据的恢复。 除此之外像tickets- -这样的操作对应的汇编指令其实至少有三条1.读取数据 2.修改数据 3.写回数据而线程函数我们知道会在每个线程的私有栈都存在一份在上面的例子中多个线程执行同一份线程函数所以这个线程函数就绝对会处于被重入的状态也就绝对会被多个线程执行今天我们假设只有一个CPU(CPU就是核心处理器芯片会集成多个核心)在调度当前进程中的线程那么线程是CPU调度的基本单位所以也就会出现一个线程可能执行一半的时候被切换下去了并且该线程的上下文被保存起来然后CPU又去调度进程中的另一个线程。 3. 在知道上面的原理之后还需要知道usleep的作用当usleep放到if分支语句的第一行时票数就出现了问题出现了负数主要是因为usleep可以将线程暂时阻塞那么CPU就会把他切换下去转而执行其他线程但需要注意的是如果被切换的线程重新调度上来时还会从上次他执行后的语句继续向下运行。 所以会出现多个线程同时进入到分支判断语句然后去阻塞等待的情况假设tickets已经变成了1然后其余的线程此时都被调度上来了他们都开始执行tickets- -- -之后不满足循环条件线程才会退出那么如果我们创建出了4个线程就会有3个线程在票数已经为0的情况下继续减减所以就会出现票数为负数的情况。 4. 而我们能够复现出问题其实主要靠的是usleep和逻辑判断与tickets- -分开那么线程就有可能在执行if逻辑判断之后还没有执行tickets- -之前就被切换下去了而多个线程都出现这样的情况时他们都被重新调度时重新加载自己的上下文数据时继续向后执行但此时tickets已经没有了共享资源tickets在多线程访问时就会出现数据不安全的问题。 5. 我们上面是将逻辑判断和tickets- -分开了那是不是只要别分开就不会出现问题呢 答案并不是这样的还是会出现问题的只不过我们复现出这样的问题需要靠概率而已所以并不是那么好复现。但我们只要知道原理就可以下面再来分析一下只有tickets- -这一步的情况下是否会出现问题呢 我举了两个线程同时循环执行票数-1的例子。如果真要说到底这些由于多线程操作共享资源而产生的问题本质原因只有一个他们可能在运行的一半被切换走了连同他自己的上下文结构而被切换走的同时其他调度上来的线程依旧可以访问这个共享资源但是被切换下去的线程不知道啊没人告诉我啊我和我的上下文就等着被CPU重新调度回去呢但等我回来的时候天都已经大变样了我还啥都不知道继续傻傻的操作共享变量此时就出现共享资源数据不一致的问题了。 2.提出解决方案加锁局部和静态锁的两种初始化/销毁方案 2.1 对于锁的初步理解和实现 1. 那该如何解决上面的问题呢多个执行流操作共享资源时发生了数据不一致问题。 解决上面的问题实际要通过加锁来实现但在谈论加锁的话题之前我们需要来重新看待几个概念。 多个执行流总是能够共享许多资源但在加锁保护后的共享资源我们称为临界资源。 而多个执行流执行的函数体内部对临界资源进行操作的代码称为临界区需要注意的是临界区不是整个函数体内部的代码而是指对共享资源进行操作的代码称为临界区。 如果我们想让多个执行流串行的访问临界资源而不是并发或并行的访问临界资源这样的线程调度方案就是互斥式的访问临界资源串行就是指只要一个线程开始执行这个任务那么他就不能中断必须得等这个线程执行完这个任务你才能切换其他线程执行其他的任务这个概念等会讲完锁之后大家就明白什么是互斥了 当线程在执行一个对资源访问的操作时要么做了这个操作要么没有做这个操作只要两种状态不会出现做了一半这样的状态我们称这样的操作是原子性的。就比如你妈让你写作业你要么给我把作业写完了再出去玩要么就一个字也别写给我滚出家门就这两种状态不会出现你写了一半然后你妈让你出去玩的这种情况这样也是原子性 2. 有了上面四组概念的稍稍铺垫之后我们来谈谈如何对共享资源进行加锁和解锁首先锁实际就是一种数据类型这个锁就像我们平常定义出来的变量或是对象一样只不过这个锁的类型是系统给我们封装好的一种类型进行重定义后为pthread_mutex_t。变量或对象在生命的时候也是可以初始化的变量初始化后就是变量的定义而不是声明了。变量和对象也都有自己的销毁方案内置类型的变量销毁时操作系统会自动回收其资源而自定义对象销毁时操作系统会调用其析构函数进行资源的回收。 锁同样也是如此锁也有自己的初始化和销毁方案如果你定义的是一把局部锁就需要用pthread_mutex_init()和pthread_mutex_destroy()来进行初始化和销毁如果你定义的是一把全局锁或静态所则不需要用init初始化和destroy销毁直接用PTHREAD_MUTEX_INITIALIZER进行初始化即可他有自己的初始化和销毁方案我们无须关心静态或全局锁如何销毁。 定义好锁之后我们就可以对某一段代码进行加锁和解锁加锁与解锁意味着这段代码不是一般的代码只有申请到锁持有锁的线程才能访问这段代码加锁和解锁之间的代码可以称为临界区因为想要访问这段空间必须有锁才可以访问。pthread_mutex_lock实际就是申请锁的代码和临界区的入口如果你申请锁成功了那么你就可以进入临界区访问临界资源如果你并没有申请成功比如当前这把锁已经被别的线程申请到并持有了其他线程正持有锁在临界区访问着呢那么你就无法进入临界区因为你并没有持有锁必须得在pthread_mutex_lock这个接口外面等着直到你申请到锁之后你才能进入临界区访问临界资源这样的线程访问实际就是互斥指的是当一个线程正在持有锁访问临界区的时候其他线程无法进入临界区直到持有锁的线程释放锁之后才会有可能进入临界区注意是有可能因为当线程释放锁之后这把锁还需要被竞争哪个线程竞争到这把锁哪个线程才能持有锁的访问临界资源 3. 上面谈论完锁的初始化和销毁以及如何加锁和解锁之后我们来利用锁解决上面出现的共享资源访问不安全的问题。你不是由于多线程再进行临界资源访问时可能由于线程切换什么的导致非原子性式的访问临界资源吗那我不让你这么干我对这段临界资源进行加锁让你当前申请到锁正在访问临界资源的线程必须给我以原子性的访问来访问临界资源换句话说你必须把访问临界资源的工作做完了才可以要么你不要访问临界资源要么你访问了临界资源就必须把临界资源全部访问完了中间不能访问一半就不访问了所以只要对临界资源进行加锁后临界资源就变得安全了因为无论什么线程想要访问临界资源都必须以原子性的方式访问完这样的话就不会出现在访问一半的时候线程被切换下去了其他线程被切换上来继续访问临界资源了而是说如果持有锁的线程被切换下去了这个线程会抱着他申请到的锁被切换下去此时其他线程如果被切换上来想要访问临界资源那也没用因为你没有锁啊持有锁的线程被切换时是抱着锁被切换的那你现在既然访问不了临界区CPU无法继续执行代码那就只能等持有锁的线程重新被切换上来时才能继续开展临界资源的访问工作这个工作必须且只能由申请到锁的线程来完成其他任何线程都无法完成这个工作反过来说这不就是原子性吗访问临界资源的工作只要被持有锁的线程开始做了哪怕他在做的过程中被切换下去了也无须担心因为别的线程做不了这个工作所以还是得等持有锁的线程被切换上来的时候才能继续做这个工作那是不是这个工作只要开始做了就一定会被做完呢会不会出现做一半停下来了不做了让别的线程在去访问临界资源的情况呢当然不会这就是锁带来的作用。 4. 如果在加锁之后运行代码实际可以发现他抢票的速度是要比没加锁之前慢的原因也很简单。我来给大家解释一下没加锁之前线程之间是可以并发或并行执行的我先大概说一下并发和并行是什么后面会详细介绍这两者的区别和概念并发你可以简单理解为当线程运行一半被切换下去的时候此时CPU还可以调度运行其他线程也就是说如果多个线程在运行的时候每个线程都会被CPU跑一跑那在一段时间内所有的线程都可以被执行到并且推进每个线程的执行过程。而并行就是在多个核心上面同一时刻跑不同的线程比如两个同时访问临界资源的线程在未加锁的时候可能出现多个核心同时执行两个线程的代码同时在访问临界资源但实际这种情况并不常见因为我们写出来的代码优先级并没有那么高所以基本上都是在按照并发执行的。 然后加锁前是并发执行的也就是说在一个线程被切换下去的时候其他- -tickets的线程还能够被重新调度上来进行票数的- -那么总体上来说票数就会被一直- -。 而加锁之后就不是并发执行的了因为我们上面说过加锁之后即使持有锁的线程被切换下去其他被调度到CPU上的线程也是无法进行票数- -的因为他们没有锁所以在持有锁的线程被切换下去的这段时间里票数不会改变因为线程在串行的访问临界资源什么是串行呢就是一个线程访问完之后才能轮到另一个线程就是我们前面说的一个线程在完成他的工作之后释放完锁之后其他线程才有可能竞争到锁才有可能访问临界资源这样就是串行。 串行的执行效率肯定要比并发执行的效率底嘛因为当多线程在执行任务的时候我们进行并发执行为的就是当前线程如果被切换下去了那也没啥事因为其他被调度上来的线程依旧可以执行这个任务。你现在加锁之后就会变成串行执行了那当前持有锁的线程被切换下去时其他被调度上来的线程是无法继续执行任务的效率自然就会底一些。效率底一点就底一点吧毕竟现在共享资源就安全了嘛下面运行结果你也可以看到没有锁的时候票数就为负数了这种情况用户怎么可能容忍。 2.2 局部和全局锁的两种加锁方案的代码实现 1. 如果定义局部锁的话我们肯定是想要将这把锁传给每个线程的让每个线程都用这把锁来互斥式的访问共享资源以此来保证共享资源的安全性。并且我还想给每个线程带上名字这样在打印结果上可以区分是哪个线程在进行抢票。 所以我们是不是需要一个结构体ThreadData来封装一下锁和线程名字呢所以我们就定义出一个结构体把结构体指针传给线程让线程能够使用锁来访问临界资源 2. 接下来我们还要看一下加锁之后的运行现象。在没有一次while循环之后的usleep(1000)时可以看到发生抢票的用户一段时间内基本都会是一个用户比如打印结果中如果是用户1抢票那大概用户1要抢比较长的一段时间的票然后才会换到其他用户这是为什么呢 因为锁只规定了线程必须互斥式的访问临界资源但并没有规定哪个线程先去执行访问临界资源的操作换句话说只要你线程拿着锁来访问临界资源那我就同意你访问我管你是哪个线程呢你有锁就行也就是说你释放完锁之后在重新竞争锁的时候如果你又能竞争到这把锁那你就一直拿着这个锁来访问就好了。你要是能一直竞争到锁那你就能一直来访问临界资源。 而下面现象我们其实可以看到刚刚释放完锁的线程在重新竞争锁的时候这个线程的竞争能力是比较强的所以就会出现下面的现象一个用户抢票之后大概还要抢很长时间的票。同时其他线程就无法抢票就只能眼巴巴的看着那个竞争能力强的线程一直在抢票这样的现象我们称为饥饿状态解决的方式实际是通过线程同步来解决的这里先预热一下后面会详细讲的。 3. 上面那种现象正确吗当然是正确的我这个线程竞争能力强嘛我凭啥不能一直抢票呢锁只规定了我要互斥式的访问临界资源又没说必须是哪个线程先进行或后进行抢票我就要一直抢票你能把我怎么样 但是上面的现象虽然是正确的但是他不河狸比如抢火车票这个票一直被一个用户抢其他用户一直都抢不着那铁路局咋赚钱呢一个用户的消费咋能养活一个铁路局呢肯定得多个用户消费啊 所以除了使用线程同步来解决之外还可以通过usleep(1000)来解决睡眠的多少不重要只要让线程在释放完锁之后睡眠一会儿将自己阻塞挂起(是否挂起是未知的取决于OS)一会儿阻塞挂起的时候其他线程不就能竞争到锁了吗那其他线程是不也可以进行抢票了就不用眼巴巴的看着竞争能力强的那个线程一直在抢票了 下面是有usleep和没有usleep的两次结果对比没有usleep时一个线程可能会霸占抢票较长时间有usleep时多个线程都可以协调的进行抢票不会出现一个线程持续霸占抢票的情况。 4. 除了上面代码使用局部锁的实现方案外我们还可以使用静态锁或全局锁局部的静态锁还是需要将锁的地址传给线程函数否则线程函数无法使用锁因为锁是局部的嘛如果是全局锁那就不需要将其地址传给线程函数了因为线程函数可以直接看到这把锁所以直接使用即可。 3.根据代码现象提出问题 3.1 如何看待锁 1. 完成上面对于共享资源访问不安全问题的解决之后我们来深入的理解一下锁。 我们知道共享资源在被多线程访问时是不安全的所以我们需要加锁来保护共享资源。但是我们回过头来想一想锁本身是不是共享资源呢所有的线程都需要申请锁和释放锁那不就是在共同的访问锁这个资源嘛所以锁本身不就是共享资源吗那多个线程在访问锁这个共享资源的时候锁本身是不是需要被保护呢当然需要其他的共享资源可以通过加锁来进行保护那锁怎么办呢 实际上加锁和解锁的过程是原子的也就是说只要你申请了锁并且竞争能力恰好足够那么你就一定能够拿到这个锁否则你就不会拿到这个锁不会说在申请锁申请一半的时候线程被切换下去了其他线程去申请锁了不会出现这种中间态的情况既然加锁和解锁的过程是原子的那其实访问锁就是安全的但加锁解锁的过程为什么是原子的呢我该如何理解呢这个后面会说。 地址空间中大部分的资源都是共享的包括锁本身这个共享资源 2. 如果申请锁成功了那线程就会继续向后执行代码进入临界区访问临界资源。那如果申请锁要是没成功呢或者说暂时申请不到锁呢执行流又会怎么样呢 下面代码中线程函数内部申请了两次互斥锁这实际就会出问题了可以看到代码不会继续运行了并且是进程内的所有线程都不会被调度没有一个线程能够进行抢票我们通过ps -aL还可以看到线程确实都存在但是都不会执行代码并且ps -axj也可以看到当前进程变成了Sl状态也就是处于阻塞状态而不是R运行状态 3. 所以如果申请不到锁执行流就会阻塞。 因为你线程申请锁的时候锁被别的线程拿走了那你自然就无法申请到锁操作系统会将这样的线程暂时处于休眠状态。只有当持有锁的线程释放锁的时候操作系统会执行POSIX库的代码重新唤醒休眠的线程让这个线程去竞争锁如果竞争到那就持有锁继续向后运行如果竞争不到那就继续休眠。 那上面为什么会出问题呢实际是因为当前线程已经申请到锁了但是他又去申请锁了而这个锁其实他自己正持有着呢但是他又不知道自己持有锁因为我们主观让线程执行了两次申请锁的语句是我们让他这么干的他自己拿着锁然后他现在又要去申请锁但锁实际已经被持有了那么当前线程必然就会申请锁失败也就是处于休眠状态什么时候他才会唤醒呢当然是锁被释放的时候当锁被释放时操作系统才会唤醒当前线程但是锁会释放吗当然是不会啦因为你自己把锁拿着你还等其他线程释放锁人家其他线程又没有锁你自己还运行不到pthread_mutex_unlock这段代码也就是说你自己又不释放锁你还让没有这个锁的线程去释放锁这不就是自己把自己给搞阻塞了吗这其实就是产生死锁了线程永远都无法等待锁成功释放那么这个线程将永远处于阻塞状态无法运行同样其他线程道理也如此 所以我们就可以看到上面那么多线程全都阻塞了每一个能跑的其实就是因为发生死锁问题了所有的线程都无法申请到锁其中大部分的线程都是因为根本就没碰到锁一直想等锁被释放从而发生的休眠而一个大傻线程是自己拿着锁呢但是还忘记自己拿着锁了要别人把锁还给他而一直等待别人释放锁从而产生的休眠问题 4. 那该如何解决呢两种办法第一种就是通过pthread_mutex_trylock()来申请锁这个接口会试着进行申请锁如果申请到锁那就继续向后执行代码运行即可。如果没有申请到锁就会立马出错返回所以这个接口实际是一种非阻塞式的申请锁的一种方式。从产生问题的原因角度解决了问题你不是要阻塞式的申请锁吗那我直接不阻塞不就得了但其实这种解决方式是非常不好的因为一个线程出问题整个进程都会退出你其他线程申请不到锁就申请不到呗但现在有一个线程申请到锁了并且互斥式的访问临界资源的呢正访问着呢因为别的线程申请不到锁就把我当前线程资源就回收了而且所有的线程还都退出了这合理吗当然不合理所以这样的解决方式不好用我们还是得用主流的lock和unlock来进行锁的申请和释放 所以对于lock申请到的锁还有另一种锁的叫法叫做挂起等待锁 那该怎么解决呢我所知道的实际并没有很好的解决办法只能我们程序员小心再小心千万不要写出死锁的代码如果一旦写出那也要通过死锁产生的问题迅速补救代码检查出死锁产生的位置进行更改代码 实际上面总结下来也就一句话谁持有锁谁才能进入临界区你没有锁那就只能在临界区外面乖乖的阻塞等待等待锁被释放然后你去竞争这把锁竞争到就拿着锁进入临界区执行代码竞争不到就老样子继续乖乖的在临界区外面阻塞等待 5. 上面我们已经理解了临界区临界资源串行执行未持有锁线程的阻塞等待以及互斥访问这样的概念。但在锁这里还有一个概念是原子性我该如何真正的理解线程持有锁的过程中原子性这样的概念呢 在谈论真正理解加锁过程中的原子性概念之前我们先来讨论几个问题。我这里就不说这些问题了大家可以看我下面画的图。实际这些问题我们早就在上面说过了无非就是未持有锁的线程会阻塞等待式的等待锁被释放和持有锁的线程在被调度切换时会拿着自己的锁被切换下去其他被重新调度到CPU上的线程依旧是无法申请到锁的因为锁只有一把而且是被刚刚切换下去的线程所持有的所以被重新调度到CPU上的线程也没啥用因为他们无法继续向后执行代码这两个话题其实上面都已经说过了我们这里就相当于做一下复盘 6. 那么对于其他未持有锁的线程而言实际有意义的锁的状态无非就两种一种是申请锁前一种是释放锁后申请锁前锁还没有被申请到那么对于其他未持有锁的线程来说当然是有意义的。释放锁后锁此时处于未被申请到的状态那未持有锁的线程当然有可能竞争到这把锁所以这也是一种有意义的状态 而我们站在未持有锁的线程角度来看的话当前持有锁的线程不就是原子的吗他们看到的锁只有在未申请前和持有锁线程释放锁之后这两种有意义的状态那这就是原子的不会出现中间态的情况。 所以在未来使用锁的时候一定要保证临界区的粒度非常小因为加锁之后线程会串行执行如果粒度非常大那么执行这段临界区所耗费的时间就越多整体代码运行的效率自然就会降下来因为其余非临界区是并发或并行执行而临界区是串行所以整体效率会由于临界区的执行效率受较大影响那么在平常加锁和解锁时我们就要保证临界区的粒度较小为此能够让程序整体的运行效率依旧保持较高的状态 7. 谈论额外的几个话题我们说未持有锁线程在等待释放锁期间会进入阻塞状态如果说具体一些的话实际这些未持有锁的线程会被放在互斥锁对应的等待队列中互斥锁对象内部维持了一个等待队列用于存放被该锁阻塞的线程。 加锁是程序员行为如果要访问共享资源那么所有访问该共享资源的线程都要加锁不能说有的线程加锁有的线程不加锁。比如现在有一批线程他们要执行两个线程函数这两个线程函数内部都会访问共享资源但一个线程函数内部对共享资源进行加锁一个没有加锁那么就会导致其中一批线程需要互斥式的串行访问共享资源而另一批线程则可以随意并发式的访问共享资源这一定会出安全问题的这算程序员写出了bug因为你对共享资源的保护不够彻底算你自己的问题 3.2 如何理解加锁和解锁的本质硬件层面和软件层面的加锁 1. 在文章的较前部分我们谈到过单纯的i和i的语句都不是原子的因为这样的语句实际还要至少对应三条汇编语句从内存中读取数据在寄存器中修改数据最后再将修改后的数据写回内存所以i和i这样的语句一定不是原子的因为他在执行的时候是有中间态的可能在执行一半的时候由于某些原因被切换下去这样就会停下来。这种非原子性的操作就会导致数据不一致性的问题也就是前面我们常谈的共享资源访问不安全的问题随之而来的解决方案就是我们所说的加锁对共享资源进行互斥式的访问以保证其安全性。 而加锁和解锁的过程实际也是访问共享资源锁的过程那么加锁和解锁是如何保证其访问锁的原子性呢答案是通过一条汇编语句来实现。 为了实现互斥锁的加锁过程大多数CPU架构都提供了swap和exchange指令该指令的作用是把寄存器和内存单元的数据进行交换因为只有一条汇编指令保证了其原子性。并且即便是多处理器平台访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期就绪后才能访问。 2. 实际上除我们语言所说的一条汇编语句交换数据而保证的原子性外在操作系统内还有另一种硬件层面上的实现原子性的简单做法。因为线程在执行过程中有可能出现线程执行一半被切换了那么线程完成任务就不是原子的了所以我们能不能让线程在执行的时候压根就不能被切换只要你线程上了CPU的贼船就不能下去必须得等你完全执行完代码之后才可以被切换下去。 至于线程在执行一半的时候被切换走原因有很多可能是时间片到了来了更高优先级的线程线程由于访问某些外设或自己的原因等等需要进行阻塞等待这些情况下都有可能在线程执行一半的时候被切换下去 所以在系统层面我们只要禁止一切中断对线程的中断不做任何响应禁止中断的总线做出任何响应关闭外部中断以达到线程不被切换下去的效果从而实现访问共享资源的原子性。 当然这样的方案比较偏底层算是一个比较重量级的方案在硬件层面实现这样的方案的话成本还是挺高的除非线程要完成的工作优先级特别高且必须是原子性的我们才会这么做否则一半情况下不会采用这样的方案来实现原子性。 3. 在谈论加锁过程的汇编代码之前我们先来谈几个共识性的话题CPU内寄存器只有一套被所有的执行流共享并且CPU内寄存器的内容是每个执行流都私有的称为运行时的上下文。可以看到加锁的汇编语句就是将0放到al寄存器内部然后就是执行只有一条的汇编语句xchgb将al寄存器的内容和物理内存单元进行数据交换此时al寄存器内容就会变为1物理内存中的mutex互斥量的值变为0将物理内存中mutex的1和al寄存器内0进行交换我们可以形象化的表示为线程A把锁拿走了在拿走锁之后线程A有没有可能被切换走呢当然有可能但线程A在切换的时候他是带着自己的上下文数据被切换走的。 此时线程B被重新调度上来后他也会先将0加载到自己上下文中的al寄存器内部然后再执行xchgb汇编语句但此时物理内存的mutex是0代表锁已经被申请了所以交换以后al寄存器内部的值依旧是0继续判断之后会进入else分支语句该线程就会由于等待锁被持有锁的线程释放而处于挂起等待的状态。 所以只要线程A申请锁成功了即使线程A的运行被中断了我们也不担心因为交换寄存器和内存的汇编语句只有一条这能保证加锁过程也就是申请锁过程的原子性。并且在线程A被切走时线程A是持有锁被切走的那么即使其他线程此时被调度上来他们也一定无法申请到锁那就必须进行阻塞等待只有重新调度线程A将线程A的上下文加载到寄存器内部此时al内容就会变为1则返回return 0代表申请锁成功线程A就可以持有锁式的访问临界区。 4. 上面说的加锁过程是原子的交换寄存器和mutex内容仅由一条汇编语句来完成而mutex是我们所说的共享资源所以一条汇编语句保证了mutex操作的原子性。 而解锁的过程也非常简单直接将1mov到mutex里面就完成了释放锁的过程然后唤醒阻塞等待锁的线程让他们现在去竞争锁因为锁已经被释放了所以同样的释放锁的汇编语句也只有一条这也能保证释放锁过程的原子性 3.3 RAII风格的封装设计锁构造函数加锁析构函数解锁 1. 如果我们想简单的封装使用锁那我们该如何设计呢我们也想像之前封装设计线程那样搞出来C式的面向对象版的创建线程和销毁线程。 实际实现起来也很简单无非就是对原生的申请锁加锁解锁接口的封装我们先定义一个互斥量的类类中实现构造函数将锁的地址进行初始化然后定义出加锁和解锁的两个接口这样就可以定义出来一个内部能够进行加锁和解锁的类。 然后我们再加一层封装实现出RAII Resource Acquisition Is Initialization风格的加锁即为构造函数处进行加锁析构函数处进行解锁 至于锁的初始化和销毁方案是类外面的事情使用时需要自己先初始化好一把锁确定初始化和销毁的方案然后利用Mutex.hpp这个小组件来进行加锁和解锁的过程 2. 在这里补充一个知识点对象的生命周期是随代码块儿的也就是说当对象离开代码块儿的时候会自动调用析构函数例如下面抢票代码中我们不想把usleep(1000)也放入到临界区因为加锁之后的代码都属于临界区了只有对象销毁时才会发生解锁所以我们就可以利用代码块儿来实现临界区的范围管控。 没有代码块就会出现刚释放完锁的线程竞争能力强持续霸占抢票导致其他线程出现饥饿问题有代码块也就是前面我们说过的在释放完锁之后让刚刚持有锁的线程停一会儿让其他线程也能竞争到锁也能进行抢票 我之前并不知道这个知识点或者说知道的并没有那么清楚像上面那种代码块儿的使用方法我倒是没有见过所以特地跑到vs上面验证了一下下面是验证结果事实确实如上面所说那样。 4.可重入与线程安全 1. 在多线程并发执行代码同时访问共享资源的时候如果某一个共享资源由于多线程访问发生了数据不一致共享资源不安全并且导致其他线程运行出问题了那么这种情况就是线程不安全的。尤其对于没有锁保护的共享资源的多线程访问的代码很大概率出现线程不安全的情况。 而什么是可重入呢这个话题并不陌生我们之前谈论进程信号的时候进程可能由于收到信号并且在陷入内核时检测到信号跳转到handler方法执行信号处理函数信号处理函数中可能会出现和main执行流中执行相同的函数体例如当时我们所说的链表的push_back在main和handler中同时执行可能会导致某些未知错误的产生如果出现了问题那么我们称这个函数是不可重入函数如果没有出现问题这个函数就是可重入函数。值得注意的是不可重入函数说的是这个函数的属性而不是说这个函数叫做不可重入函数那么他就一定不能被执行流所重入只是说他如果被执行流重入极大概率是要出问题的。 2. 下面是一些线程安全和不安全函数可重入和不可重入的话题实际就是混一堆概念写代码的时候根本用不到也就是现在在这里说一下而已。 3. 一句话可重入函数是线程安全的充分不必要条件线程函数如果是可重入的那么就一定是线程安全的反过来是不一定的。 5.死锁 5.1 死锁概念 1. 死锁是指一个进程中的各个线程都持有着锁但同时又去申请其他线程的锁而每个线程持有的锁都是占有不会释放的所以大家都会等着等对方先释放锁但是呢大家又都不释放锁全都占有着锁所以大家就会处于一种永久等待的状态也就是永久性的阻塞状态所有执行流都不会被运行这样的问题就是死锁 之前抢票的代码中多个线程使用的是同一把锁未来有些场景一定是要使用多把锁的在多把锁的情况下如果某些线程持有锁不释放还要去申请其他线程正持有的锁而每个线程都是这样的状态那就是死锁问题。 2. 一把锁有可能造成死锁问题吗当然是有可能的前面我们谈到过这个问题一个线程已经持有锁了但他又去等待这个锁释放但这个锁现在释放不了那他自己就会持有锁式的阻塞等待。其实就是一个人骑着毛驴找毛驴那他最后能找到毛驴吗当然是找不到的 3. 下面来谈一下产生死锁的逻辑链条大家看一下就好我们谈论的重点还是产生死锁的四个必要条件这里只是对死锁产生做一个解释而已。 5.2 产生死锁的四个必要条件 1. 互斥条件一个资源每次只能被一个执行流使用互斥其实就是加锁之后线程的串行执行。 请求与保持条件一个执行流由于请求资源而阻塞时对自己已经获得的资源保持不放。说白了就是我自己的东西不释放我还要你的东西你不给我就一直等等到你给我为止。 不剥夺条件一个线程在未使用完自己获得的资源之前是不能够强行剥夺其他线程的资源的。说白了就是你先在还有资源呢你想要别人的自由你就得等不能强行剥夺当你使用完自己的资源后你可以去等待申请别人的资源。总之就是不能强行剥夺其他线程的资源想要就必须阻塞等待别人释放资源才可以。 循环等待条件若干个执行流之间形成一种头尾相接的互相等待对方资源的关系。我们也称这样的现象为环路等待。 2. 破坏死锁实际就是破坏死锁的四个条件其中之一只要破坏一个条件死锁就无法产生。 第一个互斥是锁的特性我们无法改变。 在申请第二把锁的时候如果申请暂时不成功那就不去阻塞等待该锁被释放而是直接出错返回这样就破坏了保持的条件也就是说如果请求不成功也不保持自己的资源不释放了而是直接释放资源出错返回这样也能避免死锁。例如使用pthread_mutex_trylock来申请锁。 我们可以设定一个竞争策略例如优先级较高的线程可以剥夺优先级较低线程的资源也就是可以抢过来直接把优先级较低线程的锁抢过来。所以判断能否剥夺资源时我们通过优先级的高低就可以判断。 因为申请锁的顺序而导致线程出现了环路等待问题所以我们就让他们申请锁的顺序保持一致不要产生环路等待的问题。例如假设访问临界资源需要持有AB两把锁那么让所有线程申请锁的顺序都是先申请A锁再申请B锁这样的话申请A锁成功的线程一定能申请到B锁那么该线程就可以拿着这两把锁去访问临界区而其他线程由于连A锁都申请不到更别说申请B锁了所以他们就只能等待持有锁线程释放A锁这样的好处就是不会产生死锁问题。如果你不这么做那一定会导致死锁问题的产生例如一个线程先申请A锁再申请B锁另一个线程先申请B锁再申请A锁那么就会出现第一个线程一直等后一个线程释放B锁而后一个线程一直在等第一个线程释放A锁而每个线程都是请求与保持的所以最终结果就是两个线程都一直处于永久阻塞等待的状态此时就产生死锁问题。这种解决方案还是很不错的让所有线程申请锁的顺序保持一致 3. 那么如何避免死锁呢我们可以通过下面的几种方式来避免死锁这些是程序员在写代码上需要注意的一些细节。 例如资源一次性分配这样的细节如果一个接口里面大量的申请了空间资源那么就提前将这些资源申请好而不是在写代码的途中进行资源申请因为在多线程的环境下多个执行流还有锁的情况你在代码中进行资源申请是有可能出现问题的如果代码量巨大那出现的问题真是能头疼死人同样加锁的条件也会变得非常复杂。 所以在多线程环境下强烈建议要将资源进行一次性分配如果你不这么做也没关系因为代码出错之后代码会教你做人的。 4. 除上面需要注意的避免产生死锁的代码编写之外还有两个避免死锁产生的算法需要说一下。 首先提一个问题一个线程申请的锁另一个线程可以释放这个锁吗当然是可以的释放锁不就是调用一下unlock接口嘛哪个线程不能做这个工作啊只要把对应锁的地址传给任意一个线程该线程都可以通过调用unlock接口来释放锁。所以一种死锁检测的算法思想就是定义一个类类里面定义计数器这个计数器衡量的是每个线程是否运行只要线程运行那么这个计数器就会一直然后可以用另一个监控线程盯着这个计数器一旦计数器长时间不变化就有可能产生死锁此时监控线程负责将锁unlock释放通过直接释放锁的方式来避免产生死锁。 银行家算法了解 5. 即使教材上面对于死锁的解决方案说的非常详细但实际在工程中能不用锁尽量不要用锁如果非常必须用锁来解决问题那也要尽量少的锁来解决问题。因为这个锁和C的模板一样水很深我们并不能因为我们正在学这个东西那这个东西就一定是重要的或者是实际中使用率较高的这不是绝对的。 二、线程同步生产消费模型 1.通过条件变量抛出线程同步的话题 1. 我们前面就说过在抢票逻辑中刚释放完锁的线程由于竞争能力比较强导致其他线程无法申请到锁那么长时间其他线程都无法申请到锁只能阻塞等待着这样的线程处于饥饿状态 我们可以举一个例子来理解条件变量是如何实现线程同步的。 假设现在学校开了一间学霸vip自习室学校规定这间自习室一次只能进去一个人上自习自习室门口挂着一把钥匙谁来的早先拿到这把钥匙就可以打开门进入自习室学习并且进入自习室之后把门一反锁其他人谁都不能进来。然后你第二天准备去学习了卷的不行直接凌晨三点就跑过来拿着钥匙进入自习室上自习了然后卷了3小时之后你想出来上个厕所一打开门发现外面站的一堆人都在叽叽喳喳的讨论谁先来的怎么来的这么早这么卷然后你怕自己等会儿把钥匙放到墙上之后上完厕所回来之后有人拿着钥匙进入了自习室你就又卷不了了所以你把钥匙揣兜里拿着钥匙去上厕所了其他人当然进入不了自习室因为你拿着钥匙去上厕所了。等你回来的时候你又打开门又来里面上了3小时自习你感觉自己饿的不行了在不吃饭就饿死在里面了所以你打开门准备出去吃饭了然后突然你自己感觉负罪感直接拉满我凌晨3点好不容易抢到自习室现在离开是不太亏了所以你又打开自习室回去上自习去了别人当然竞争不过你呀因为钥匙一直都在你兜里你出来之后把钥匙放到墙上你发现有点负罪感你又拿起来钥匙回去上自习因为你离钥匙最近所以你的竞争能力最强。结果你来自习室上了1分钟自习又出来了然后又负罪的不行又回去了周而复始的这么干结果别人连自习室长啥样都没见到。 像这样由于长时间无法得到锁的线程没办法进入临界区访问临界资源我们称这样的线程处于饥饿状态 2. 所以学校推出了新政策所有刚刚从自习室出来的人都必须回到队列的尾部重新排队等待进入自习室这样的话其他人也就可以拿到钥匙进入自习室了。 所以在保证数据安全的前提下让线程能够按照某种特定的顺序来访问临界资源从而有效避免其他线程的饥饿问题这就叫做线程同步 2.生产消费模型的概念理解321原则 1. 上面我们已经初步理解了条件变量带来的作用那就是让互斥访问的线程能够实现同步有效避免其他线程的饥饿问题但在真正学习使用条件变量之前我们还需要再来谈论一个模型叫做生产消费模型在谈论完生产消费模型之后我们在来使用一下条件变量然后基于条件变量生产消费模型实现出一个基于阻塞队列式的生产消费模型代码。 2. 实际生活中我们作为消费者一般都会去超市这样的地方去购买产品而不是去生产者那里购买产品因为供货商一般不零售产品他们都会统一将大量的商品供货到超市然后我们消费者从超市这样的交易场所中购买产品。 而当我们在购买产品的时候生产者在做什么呢生产者可能正在生产商品呢或者正在放假呢也可能正在干着别的事情所以生产和消费的过程互相并不怎么影响这就实现了生产者和消费者之间的解耦。 而超市充当着一个什么样的角色呢比如当放假期间消费爆棚的季节中来超市购买东西的人就会非常的多所以就容易出现供不应求的情况但超市一般也会有对策因为超市的仓库中都会预先屯一批货所以在消费爆棚的时间段内超市也不用担心没有货卖的情况。而当工作期间大家由于忙着通过劳动来换取报酬可能来消费的人就会比较少商品流量也会比较低那此时供货商如果还是给超市供大量的货呢虽然超市可能最近确实卖不出去东西但是超市还是可以把供货商的商品先存储到仓库中以备在消费爆棚的季节时能够应对大量消费的场景。所以超市其实就是充当一个缓冲区的角色在计算机中充当的就是数据缓冲区的角色。 而计算机中哪些场景是强耦合的呢其实函数调用就是强耦合的一个场景例如当main调用func的时候func在执行代码的时候main在做什么呢main什么都做不了他只能等待func调用完毕返回之后main才能继续向后执行代码所以我们称main和func之间就是一种强耦合的关系而上面所说的生产者和消费者并不是一种强耦合的关系。 3. 如果深度挖掘一下生产消费模型超市其实就是典型的共享资源因为生产者和消费者都要访问超市所以对于超市这个共享资源他在被访问的时候也是需要被保护起来的而保护其实就是通过加锁来实现互斥式的访问共享资源从而保证安全性。 在只有一份超市共享资源的情况下生产和生产消费和消费以及生产和消费都需要进行串行的访问共享资源。但为了提高效率我们搞出了同步这样的关系因为有可能消费者一直霸占着锁一直在那里消费但实际超市已经没有物资了此时消费者由于竞争能力过强也会造成不合理的问题因为消费者消费过多之后应该轮到生产者来生产了所以对于生产者和消费者之间仅仅只有互斥关系是不够的还需要有同步关系。 4. 从生产消费模型中可以提取出来一个321原则。即为3种关系两个角色1个交易场所。对应的其实是消费线程和消费线程的关系消费线程和生产线程的关系生产线程和生产线程的关系交易场所就是阻塞队列blockqueue。而实现线程同步就需要一个条件变量比如生产者生产完之后超市给消费者打个电话让消费者过来消费消费完之后超市在给生产者打个电话让生产者来生产这样就不会存在由于某一个线程竞争能力过强一直生产或一直消费的情况产生从而导致其他线程饥饿的问题。 5. 所以总结一下生产消费模型都有哪些好处。 a.他实现了生产和消费的解耦使他们之间并不互相影响。 b.支持生产和消费一段时间的忙闲不均的问题。因为缓冲区可以预留一部分数据进行数据的缓冲。 c.由于生产和消费的互斥与同步关系提升了生产消费模型的效率。 但我其实还有一个问题生产和消费是互斥的关系那生产者生产的时候消费者就不能消费因为共享资源需要被加锁保护而锁只有一把所以每次只能有一个线程访问这个共享资源那你凭什么说生产消费模型就高效了呢这个问题很重要后面讲完阻塞队列的代码实现之后要重点谈一下这个问题 3.条件变量实现线程同步的原理条件变量内部维护了线程的等待队列能wait线程也能wakeup线程 1. 为了能够让多线程协同工作就需要实现多线程的同步关系为了维护同步关系就需要引入条件变量。那条件变量是一个什么东西呢他其实和互斥锁一样都是一个数据类型定义出来的对象。初始化和销毁方案和互斥锁一模一样。唯一不同的是条件变量在使用时有两个高频使用的接口一个是pthread_cond_wait该函数的作用是将等待某一个具体锁的线程放入条件变量的等待队列中进行等待另一个是pthread_cond_signal该函数的作用是唤醒条件变量中等待队列的第一个等待线程另一个用的不怎么高频但也偶尔会用一下的接口就是pthread_cond_broadcast该函数将条件变量中的所有等待线程都会唤醒让所有线程重新回归竞争锁的状态。而不是像signal那样唤醒cond队列中任意一个阻塞等待锁的线程。 2. 除了之前我们举的自习室的例子之外下面又举了一个面试官面试求职者的例子其实说这么多例子就是为了让大家感受到条件变量所带来的作用它能够让所有互斥访问的线程都能够按照某种顺序进入临界区访问临界资源这就是环境变量带来的最大的作用。既能保证共享资源访问的安全性又能保证所有线程都可以拿到锁去访问共享资源避免出现线程饥饿的问题。所以下面的例子大家看一下就好如果你已经深刻的认识到条件变量带来的好处和作用以及他所实现的线程同步的话你可以直接忽略这段文字跳转到下面条件变量实现同步的原理部分。 3. 我们可以将条件变量理解为一个结构体它内部会有一个字段专门表示当前线程等待的锁的使用情况如果status有效那么代表此时锁也被释放还有一个字段是专门维护等待某一个锁的线程队列。当status变为有效的时候我们可以调用pthread_cond_signal唤醒cond内部的等待队列中的某一个线程将这个线程的上下文加载到CPU的寄存器上并且这个线程会申请到上一个线程释放的锁然后这个线程就可以拿着锁互斥的去访问临界区了。 所以条件变量实现同步的根本原因就是通过wait和signal来实现的比如某一个线程释放完锁了那你这个线程就不要再给我继续申请锁了因为我要唤醒cond的等待队列中的线程了他们还想要这把锁呢至于你就去cond的等待队列中等着就行了等下次唤醒到你的时候你才有资格重新申请锁。所以通过条件变量等待和唤醒的这样一种方式成功实现了多个线程都能互斥式的访问临界区而不会出现某些线程无法申请到锁而产生的饥饿问题。 4.串行、并发、并行的概念 1. 接下来我要给大家介绍几个概念是关于串行、并发、并行的。单独说这几个概念实际并不难但他们在现代计算机中是如何被分配的这样的知识就比较珍贵了。另外需要说一点的是网上有很多都喜欢把多核叫做多CPU但是吧这么叫确实没什么太大的错误因为一个处理器芯片上集成了多个核心每个核心都有自己独立的存储单元控制单元算术逻辑单元所以每个核心都可以跑不同的任务从功能角度来讲确实可以叫做多CPU但是也容易误导萌新啊就比如我这样的我以为是真的多CPU处理器呢原来是大部分人的叫法不同而已。 2. 实际我们的计算机在工作时是一定要进行并发的因为并发能很好解决用户同时想要运行多个程序的需求也就是我们所说的多任务处理但同时也需要进行并行。就比如上面图中举得例子每个大核跑不同的程序但同时某一个大核在跑程序时也可以时间片轮转的去执行另一个程序所以并行和并发在计算中是同时存在的。 而并发一定要比并行效率高的前提是多任务情况如果你站在多任务处理的角度去看待串行和并发你一定可以理解为什么并发效率要更高因为串行在线程被切换下去或者等锁被释放的时候这段时间CPU什么都做不了那这段时间就会被白白浪费掉在多任务处理的情况下效率一定就会下降。而对于并发来讲如果某个线程被切换下去或者他在等待锁被释放的时候是完全没有关系的因为CPU会调度运行其他线程所以被切换下去的线程在等待的时候时间完全不会被浪费掉而是会被CPU利用起来去跑其他的线程。 我以前不能理解为什么并发要比串行执行效率高的原因就是因为我当时站的角度并不是多任务处理而是单任务处理的角度但这种场景一定非常少见或者可以几乎说完全不存在你想一下你的电脑开机之后会只有一个任务再被单独处理吗绝对不会怎么验证呢非常简单你打开你的任务管理器去看一下有多少后台进程正在被运行这会是单任务处理的场景吗 我当时理解有误就是绝对单独一个任务无论是串行还是并发执行效率都是一样的但这个理解本身并没有错误只不过这样的场景不存在我们讨论这些线程执行效率的前提几乎都是默认在多任务处理的前提下进行讨论的 5.条件变量的基本代码编写 1. 这里我们先用全局的互斥锁和条件变量进行简单的代码测试帮助大家在代码层面上理解一下条件变量带来的效果真正使用条件变量和生产消费模型编写代码的环境放在第三部分进行讲解。 首先我们创建出一批线程并在线程函数内部对共享资源tickets进行加锁保护和使用条件变量来实现线程之间的同步关系。在start_routine中我们让所有的线程在进入临界区之后先去执行等待让所有的线程都去条件变量里面等着实际执行pthread_cond_wait时会自动以原子性的方式释放当前线程持有的锁然后由主线程来负责唤醒cond中的等待线程如果是这样的话那所有的线程都可以申请到锁访问到临界区不会出现饥饿线程。 2. 当主线程调用pthread_cond_signal唤醒cond队列中等待的线程后可以看到线程抢票的运行结果非常有顺序的执行票数- -执行的顺序是12453并且每个线程都兼顾到位没有出现线程饥饿无法执行票数- -的情况产生。 主要还是因为当线程被唤醒访问完临界资源释放完锁之后循环执行代码他又会去执行pthread_cond_wait了此时就又会释放锁进入等待队列而signal此时会继续重新唤醒等待队列的其他线程。以这样的方式来让所有线程都可以申请到锁。 这里在补充介绍一个接口pthread_cond_timedwait该接口与pthread_cond_wait不同的是wait接口会将阻塞等待锁的线程放入cond的等待队列里面直到有锁被释放时pthread_cond_signal接口会唤醒cond等待队列中的线程。而timedwait是等待锁一段时间后如果锁未被释放那么该接口会自动超时返回防止线程长时间的阻塞等待锁。但这个接口并不常用我们还是重点使用pthread_cond_wait接口。 3. 当调用pthread_cond_broadcast时会唤醒cond阻塞队列中的所有等待线程然后这批线程会依次按照某种顺序竞争锁当线程使用完锁访问完临界区之后就会释放锁然后重新回到条件变量中进行等待而此时剩余被唤醒的线程再去竞争锁做着上一个线程同样的工作。所以打印结果如下图所示唤醒一批线程之后5个线程都抢票每次都是以5个线程为单位进行唤醒。 这就是条件变量带来的线程同步让所有线程先去条件变量中进行等待随后会唤醒其中的每一个线程唤醒后的线程在访问完临界资源后又会重新投入等待队列当中以这样的方式来让所有线程都能够申请锁访问到临界区的临界资源。 三、基于blockqueue的生产消费模型 1.双阻塞队列的多生产多消费模型的实现 1. 上面我们已经谈论过生产消费模型的概念和条件变量的代码实现现在我们就要用这两样工具实现出基于阻塞队列的生产消费模型。 原本的计划是先将单生成单消费一个阻塞队列实现的生成消费模型但是吧这样有点简单了我们直接上难点的越难才能越加深大家对线程同步与互斥阻塞队列条件变量的使用等等的理解所以我们直接实现下面那种生产消费模型的代码即为多生产多消费并且实现两个阻塞队列在这种复杂环境下依旧能够保持线程间的同步与互斥式的访问共享资源。 2. 由于要实现两个分别存放不同任务的阻塞队列那我们直接就写出来一个阻塞队列的类模板这样就可以存放任意类型的对象所以下面我们先来完善BlockQueue.hpp文件的代码也就是阻塞队列的类模板代码。 我们需要一把锁来保证阻塞队列这个共享资源访问的安全性并且生产线程不满足生产条件时比如阻塞队列已经满了则生产线程此时就不应该继续生产而是要去cond的队列中进行wait直到消费线程唤醒生产线程所以生产线程要有自己的produce cond简称pcond。反过来对于消费者来说同样如此所以消费者在不满足消费条件的时候也要去自己的cond队列中进行wait那么消费者也应该要有自己的consume cond简称ccond。所以类BlockQueue的私有成员应该包括_mutex互斥锁_ccond_pcond两个条件变量我们还需要一个变量来描述阻塞队列的容量大小也就是_maxcap然后再加一个STL容器queue T _q;然后希望定义出来的所有阻塞队列的最大容量都是同一个的所以_maxcap定义为一个不可修改的静态成员变量静态变量在类内只是声明类外进行初始化初始化时需要带上类名不用添加static关键字。 阻塞队列需要实现的接口主要为四部分构造函数内需要初始化好互斥锁以及两个条件变量因为阻塞队列所使用的锁和条件变量是局部的(对象本身就在函数栈帧中)条件变量和锁那么就需要在构造函数内进行初始化在析构函数内完成销毁。 除此之外还需要实现push和pop两个接口为了保证向队列中push元素的安全性所以接口中要进行加锁和解锁然后就是判断是否满足push的条件如果队列已经满了那就不要继续push也就是不要继续生产了而是去pcond的队列中进行wait一旦wait执行流就会阻塞停下来等待被唤醒如果满足条件那直接用STLqueue的push接口push元素即可非常简单。push元素之后我们就该唤醒消费线程了因为现在队列中至少有一个元素是可以供消费者消费的所以直接调用pthread_cond_signal唤醒ccond的队列中的线程即可。最后就是释放锁的步骤。 对于pop来说由于STLqueue的pop接口不会返回pop出来的元素所以我们需要通过输出型参数的方式拿到pop出来的元素值。与push的实现逻辑一样pop满足的条件是队列中元素必须不为空如果为空则需要去ccond的队列中进行等待直到被生产线程唤醒。pop数据之后队列中一定至少有一个空的位置所以此时应该唤醒生产线程让生产线程进行元素的push最后还是不要忘记释放锁。 对于接口的实现大致逻辑说的差不多了。但在代码中还有几个细节需要特别说明一下。我们知道pthread_cond_wait接口是放在临界区内部的所以在执行wait代码之前线程是持有锁的为了在线程等待期间其他线程也能申请到锁并进入临界区所以在pthread_cond_wait被调用的时候它会自动的以原子性的方式将锁释放并将自己阻塞挂起到pcond的队列中。那么当队列中的某一个线程被唤醒的时候他还是要从pthread_cond_wait开始向后执行所以此时他还是在临界区内部所以在pthread_cond_wait返回的时候会自动重新申请锁然后继续在临界区中向后执行代码。另外判断逻辑的语句必须是while不能是if因为在多生产多消费的情景下可能出现伪唤醒的情况比如broadcast唤醒所有生产线程但实际空位置只有一个所以此时在唤醒之后某一个线程竞争到锁放入元素之后队列已经满了然后他释放了锁其他某一个线程在竞争到锁之后如果是if逻辑那就不会重新判断是否满足而是直接push元素那就会发生段错误越界访问所以要用while循环来判断保证唤醒的线程一定是在条件满足的情况下进行的push元素。至于唤醒对方和释放锁的顺序怎么样都可以因为唤醒对方对方没锁的话还是需要阻塞等待锁被释放而如果先释放锁的话由于对方没有被唤醒那照样还是拿不到锁所以这两个接口的调用顺序并不影响接口的功能所以先写谁都可以。 3. 主函数上层调用的逻辑就是要创建出多生产多消费的线程出来而且要使用两个阻塞队列来完成计算任务和保存任务的产生与消费所以我们又封装了一个BlockQueues类类中封装两个Blockqueue一个存储计算任务一个存储保存任务任务其实就是类对象所以BlockQueues的类模板参数分别为C calculate和S save。然后就是创建出阻塞队列和多个生产线程和消费线程以及保存线程。分别对应执行的线程函数是produceconsumesave然后把BlockQueues类型的指针传给三个线程函数这样在线程函数内部就可以通过BlockQueues类的两个指针成员去调用阻塞队列中的push和pop接口完成任务的push和pop。 produce中我们需要定义出CalTask类的对象把这个任务对象push到c_bq(calculate blockqueue)这个阻塞队列中构造对象需要两个操作数以及操作运算符还需要传一个mymath执行计算的函数指针进去因为我们希望这些任务对象都是可调用对象消费者在消费的时候从队列中拿到任务之后就可以通过调用()运算符重载来完成计算任务mymath函数的调用为了在打印的时候我们看的更加清晰CalTask类内还实现了toTaskString函数其实就是打印出计算任务的名称是什么比如是11?这样的名称让我们在终端能够明显看到是produce线程函数在被执行。由于操作运算符有多种所以定义出了字符串对象oper包括了5种运算符然后我们又rand生产随机数模拟两个操作数的生成。 consume中任务比较艰巨他需要消费计算任务CalTask还需要生产保存任务SaveTask到s_bq(save blockqueue)保存阻塞队列中消费任务需要传输出型参数也就是一个空的CalTask对象t到pop接口中然后pop结束后t对象即为c_bq中取出的任务对象拿出队列中的CalTask对象后想要消费其实很简单因为这个对象实际是仿函数对象直接通过()调用即可。然后就是生产保存任务到阻塞队列中与计算任务相同的是保存任务对象也需要实现为可调用对象这样在save线程取出任务对象时也可以直接通过()来调用SaveTask类中的运算符重载函数实现任务对象的消费。所以在构造SaveTask任务对象时需要传计算任务的名称也就是一个string类型的对象以便于执行保存任务到文件中时我们能在文件当中看到对应保存的计算任务名是什么然后还需要传一个函数指针Save该函数的功能其实就是进行文件操作将计算任务的名称保存到磁盘文件中。 save中道理也是相同要想拿出s_bq中的保存任务对象自然需要通过输出型参数来拿出所以我们传一个SaveTask类的空对象t到s_bq中的pop接口pop调用之后t就是s_bq中取出的保存任务可调用对象所以消费的时候直接通过()来调用SaveTask类中的()重载函数即可完成保存任务相对应的计算任务的名称就会保存在磁盘文件中。 三个线程函数的具体实现我们说完了同样的在MainCp.cc这个文件当中也有一些细节要注意。记得我们在谈论如何避免产生死锁问题时我们说到过一个写代码时需要注意的点就是在多线程编程尤其是加锁的代码中尽量将申请的资源统一在开头处一遍就申请好不要在代码中需要的时候才去申请因为那可能会出现一些你根本无法预料的错误。害害害人教人教不会事教人一教一个准没错我就是那个不在开头一遍申请好资源的人所以我也遇到了我无法解决的bug确实令我头疼了很长时间。初始化第二个阻塞队列的那行代码如果放在创建produce和consume线程之后也就是我注释掉的那个地方你去运行吧保证爽死你你看到的运行结果就会是一会儿运行正常一会儿报段错误这对于刚接触多线程的萌新来说友好度直接拉满。产生那样现象的原因是因为如果主线程运行的足够快那就会出现consume线程还没将保存任务放到s_bq之前主线程的s_bq正好初始化好了所以程序会正常运行。但如果主线程稍微运行的满了那就会出现s_bq还未初始化好consume线程就已经将保存任务放到s_bq里面了但s_bq是还没分配内存的野指针所以此时就会报段错误因为我们访问了野指针。所以老铁们尽量在开头的时候把需要使用的空间资源就分配好别等到使用的时候才去分配因为多线程不好找错误啊 4. 最后一个文件就是Task.hpp这个文件就是我们要实现的计算任务类和保存任务类以及计算的方法和保存的方法。计算任务类中要实现两个构造函数一个是空的构造函数用于main中构造出空对象作为输出型参数传递给阻塞队列的pop接口另一个就是构造出真正的任务对象。类成员只需要两个操作数一个操作符外加一个包装器即可因为包装器可以包装很多可调用对象所以如果你想搞一个仿函数对象或者lambda表达式或函数指针来传给构造函数的话包装器类型都是可以接收的在构造函数内部将这些私有成员都初始化好即可。除此之外还需要实现一个()运算符重载和一个返回string任务名的toTaskString函数为了将可调用对象的计算结果返回()运算符重载内部回调了mymath的方法将计算结果通过snprintf函数进行字符串格式化到buffer里面然后用buffer构造出string对象进行函数返回。toTaskString也是将计算任务进行名称的格式化到buffer里面同样返回一个由buffer构造出的string对象。 mymath函数的实现我就不说了用switch case语句就可以实现两个操作数的计算这真的可以算是入门级的代码实现了。 SaveTask类成员变量包括保存的计算任务名_message这个任务名实际就是通过CalTask的()运算符重载函数返回的string对象传到我们的SaveTask内的构造函数里的另一个成员变量就是包装器用于包装将任务名写到文件的文件操作方法Save函数指针。同样的还需要实现一个空的构造函数用于main中调用pop时将任务写到输出型参数空的SaveTask对象里。为了实现任务的消费我们也实现出一个()运算符重载老样子回调一下包装器包装的可调用对象即可。 至于Save的实现也不难就是比较正常的C语言文件操作fopen打开文件fclose关闭文件fputs写入文件。 5. 到此为止我们就谈完了整个的双阻塞队列实现的多生产多消费模型下面是程序的运行结果我们很好的实现了计算任务的生产消费保存任务的生产消费且是在多个生产者多个消费者的多线程情景下实现的生产消费模型。而能够实现的原因还是因为我们有锁来保证多线程访问共享资源的互斥性还有条件变量来保证多线程在互斥访问共享资源时的同步性。 2.生产消费模型高效在哪里不影响其他多线程并发或并行的获取任务和执行任务 1. 上面代码写完了我们要来回答一个非常重要的问题就是为什么生产消费模型是高效的我并没有见到他高效在哪里啊访问阻塞队列这个共享资源时不还是得互斥式的访问么你凭什么说生产消费模型高效呢 确实你说的没有问题很正确但实际生产消费模型根本不是高效在向阻塞队列中放元素和从阻塞队列中拿元素。而是高效在某一个线程在向阻塞队列中放任务的时候不会影响其他线程获取任务某一个线程在从阻塞队列中拿任务的时候不会影响其他线程在执行任务。 我们今天所写的阻塞队列中不过是存储了一些微不足道的计算任务或保存任务执行和获取起来根本不费力但未来线程在真正获取某些大型任务比如从数据库网络外设拿来的用户数据需要处理呢那在获取任务和执行任务的时候会很费时间的。 而生产消费模型高效就高效在你某一个线程互斥式的从阻塞队列中拿任务或取任务时根本就不会影响我其他多个线程在获取任务或执行任务并且其他多个线程是在并发或并行的执行任务效率是很高的 所以总结起来就一句话生产消费模型并不高效在放任务到阻塞队列和从阻塞队列拿任务而是真正高效在某一个线程拿或放任务到blockqueue的时候并不会影响其他线程并发或并行的获取任务和执行任务。
http://www.dnsts.com.cn/news/201298.html

相关文章:

  • 邮箱域名可以做网站吗中国机加工网
  • 东莞做门户网站成品网源码7w8w
  • 有专门做预算的网站没怎么查询网站的空间商
  • 优秀的摄影作品网站做网站国内阿里云虚拟主机多少钱
  • 潍坊建设网站多少钱网站开发的需求分析
  • 网站自动识别手机代码网站备案的是空间还是域名
  • 网站建设网上商城管理系统考生端重置密码
  • 顺德公益网站制作科学做视频网站
  • 服装网站设计策划wordpress主题 制作教程
  • 网站开发成本会计科目网站建设实习业务介绍
  • 黑龙江省建设网站首页wordpress 获取头像地址
  • 如何选择网站域名建一个门户网站要多少钱
  • 网站开发与制作工资上海闵行区邮编
  • 汉中市网站建设公司常州城乡建设局网站
  • 个人建网站要花多少钱深圳平面设计招聘
  • 番禺网站Wordpress的htaccess在哪
  • 深圳网站建设效果常熟市住房和城乡建设局网站
  • 河南手机网站建设公司排名53货源网下载app
  • 交网站建设 域名计入什么科目北京企业建站
  • 网站推广应该注意什么京东关键词优化技巧
  • 中国建设招标网是私人网站吗沈阳百度网站排名
  • 手机传奇网站网页制作电子教程
  • 很那网站建设网站更新了
  • 做预约的网站大连男科医院排名表
  • 商业网站西安有什么好玩的景点
  • 网站建设公司株洲官方手表网站
  • 网站开发如何使用APIlnmp wordpress 邮件
  • 北京网站优化哪家公司好自学网站建设作业
  • 网站有备案是正规的吗服装网站目标
  • 免费建域名网站Dw做网站怎么加logo