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

网站建设中的财务预算腾讯广告代理商

网站建设中的财务预算,腾讯广告代理商,江门站官网,vps用什么软件做网站文章目录 linux多线程1. 相关概念1.1 线程概念详解线程 VS 进程线程的优点线程的缺点线程异常线程用途 1.2 页表详解 2. 线程控制2.0 POSIX线程库深入理解线程库使用C多线程接口在Linux环境创建多线程 深入理解线程id线程局部存储 2.1 线程的创建 - pthread_create线程创建时参… 文章目录 linux多线程1. 相关概念1.1 线程概念详解线程 VS 进程线程的优点线程的缺点线程异常线程用途 1.2 页表详解 2. 线程控制2.0 POSIX线程库深入理解线程库使用C多线程接口在Linux环境创建多线程 深入理解线程id线程局部存储 2.1 线程的创建 - pthread_create线程创建时参数传一个对象线程创建时参数传一个字符串 2.2 线程的等待 - pthread_join2.3 线程的终止2.3.1 线程函数return2.3.2 线程终止 - pthread_exit2.3.3 线程取消 - pthread_cancel 2.4 获取线程自身的ID - pthread_self2.5 线程的分离 - pthread_detach 3. 线程互斥3.1 举例 — 多线程中全局变量并发访问的问题3.2 进程线程间的互斥相关背景概念3.3 举例 — 多线程并发抢票3.4 互斥量mutex3.4.1 互斥量的接口初始化互斥量静态分配动态分配 销毁互斥量互斥量加锁和解锁 3.4.2 互斥量实现原理 3.5 加锁后的多线程抢票3.5.1 在3.1中的代码下加锁3.5.2 优化加锁后的多线程抢票 3.6 封装原生线程库和锁3.6.1 封装原生线程库3.6.2 封装锁 - RAII风格的锁3.6.3 使用自己的封装实现多线程抢票 4. 可重入VS线程安全4.1 概念4.2 常见的线程不安全的情况4.3 常见的线程安全的情况4.4 常见不可重入的情况4.5 常见可重入的情况4.6 可重入与线程安全联系4.7 可重入与线程安全区别 5. 常见锁概念死锁死锁四个必要条件避免死锁验证: 一个线程申请锁一个线程释放锁 6. 线程同步6.0 引入6.1 同步概念与竞态条件6.2 条件变量6.2.1 条件变量的函数初始化静态分配动态分配 销毁等待条件满足唤醒等待 6.2.2 条件变量使用示例 7. 生产者消费者模型7.0 引入321原则cp问题思路 深入理解为何要使用生产者消费者模型生产者消费者模型优点 7.1 基于BlockingQueue的生产者消费者模型BlockingQueue7.1.1 单生产单消费模型(1)blockQueue.hpp(2)main.cc(阻塞队列放整数)(3)task.hpp(4)main1.cc(阻塞队列放对象[任务]) 7.1.2 多生产多消费模型main2.cc 7.2 生产者消费者模型真的高效吗 8. POSIX信号量8.1 概念理解8.2 接口介绍8.2.1 初始化信号量8.2.2 销毁信号量8.2.3 等待信号量(P操作--)8.2.4 发布信号量(V操作) 8.3 基于环形队列的生产消费者模型8.3.1 准备工作(1)环形队列介绍(2)构建cp问题 8.3.2 代码(1) 单生产单消费模型1 RingQueue.hpp2 main.cc(环形队列放整数)3 task.hpp4 main1.cc(环形队列放任务) (2) 多生产多消费模型0 分析1 RingQueue.hpp2 main2.cc 9. 线程池9.1 线程池介绍9.2 线程池代码9.2.1 v1版本(1)threadPool_v1.hpp(2)main1.cc 9.2.2 v2版本(1)threadPool_v2.hpp(2)main2.cc 9.2.3 v3版本(1)threadPool_v3.hpp(2)main3.cc 9.2.4 v4版本(基于懒汉方式实现单例模式的线程池)(0)单例模式介绍(1)threadPool_v4.hpp(2)main4.cc 10. STL,智能指针和线程安全10.1 STL中的容器是否是线程安全的?10.2 智能指针是否是线程安全的? 11. 其他常见的各种锁(了解)自旋锁 13. 读者写者问题读写锁 linux多线程 1. 相关概念 1.1 线程概念详解 教材观点: 线程是一个执行分支执行力度比进程更细调度成本更低(不用对cache进行切换)线程是进程内部的一个执行流 内核观点: 线程是CPU调度的基本单位进程是承担分配系统资源的基本实体。 那么是不是所有的操作系统都是像上面那样干的呢不是 有了线程之后OS要不要管理线程呢必须要如何管理呢先描述再组织 windows操作系统: 会给线程创建一个个TCB(线程控制块),它属于进程的PCB通过调度进程来进行线程的调度即windows内核中有真正的线程 linux操作系统: linux内核设计者想法: 复用进程的pcb结构体用pcb模拟线程的tcb不就行了很好地复用了进程的设计方案所以linux没有真正意义上的线程而是用进程方案模拟的线程 。 复用代码和结构比较简单好维护效率更高也更安全。— linux可以不间断的运行。— 实际上一款OS, 使用最频繁的功能除了OS本身下来就是进程了。 那么如何理解以前学习过的进程 线程 VS 进程 进程是资源分配的基本单位 线程是调度的基本单位 线程共享进程数据但也拥有自己的一部分数据: 线程ID一组寄存器栈errno信号屏蔽字调度优先级 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境: 文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id 进程和线程的关系如下图: 验证: 全局变量在多线程中, 我们的多线程看到的是同一个变量 #includeiostream #includesignal.h #includeunistd.h #includestdlib.h #includesys/types.h #includesys/wait.h #includepthread.h using namespace std;int g_val0; //全局变量在多线程场景中, 我们多线程看到的是同一个变量!void*thread1_run(void*args) {while(1){sleep(1);cout t1 thread... getpid() g_val: g_val g_val g_val endl;} }void*thread2_run(void*args) {while(1){sleep(1);cout t2 thread... getpid() g_val: g_val g_val g_val endl;} }int main() {pthread_t t1,t2,t3;pthread_create(t1,nullptr,thread1_run,nullptr);pthread_create(t2,nullptr,thread2_run,nullptr);while(1) {sleep(1);cout main thread... getpid() g_val: g_val g_val g_valendl;} }运行结果: 因为执行流看到的资源是通过地址空间看到得的多个LWP(线程)看到的是同一个地址空间。所以所有的线程可能会共享进程的大部分资源。 线程的优点 创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 计算密集型应用: 加密解密文件压缩和解压等与算法有关的。 — CPU资源。这里的线程越多越好吗不是一定要合适(进程/线程CPU的个数/核数一致) I/O密集型应用: 下载上层IO主要消耗IO资源磁盘的IO网络带宽等。这里的线程越多越好吗不是可以比较多 — 量化 线程的缺点 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高 编写与调试一个多线程程序比单线程程序困难得多 线程异常 单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出 如下这样的代码, 在一个主线程中创建两个线程让其中一个线程崩溃 #includeiostream #includesignal.h #includeunistd.h #includestdlib.h #includesys/types.h #includesys/wait.h #includepthread.h using namespace std;void*thread1_run(void*args) {while(1){sleep(1);cout t1 thread... getpid() endl;} }void*thread2_run(void*args) {char*shello;while(1){sleep(1);cout t2 thread... getpid() endl;*sH; //让这一个线程崩溃} }int main() {pthread_t t1,t2,t3;pthread_create(t1,nullptr,thread1_run,nullptr);pthread_create(t2,nullptr,thread2_run,nullptr);while(1) {sleep(1);cout main thread... getpid() endl;} }运行结果: 发现现象: 在多线程程序中任何一个线程崩溃了最后都会导致进程崩溃 为什么呢 系统角度: 线程是进程的执行分支线程崩溃了就是进程奔溃了 信号角度: 页表转换的时候MMU识别写入权限没有验证通过MMU出现异常被OS识别到会给进程发信号(结合1.2) 线程用途 合理的使用多线程能提高CPU密集型程序的执行效率合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是多线程运行的一种表现 1.2 页表详解 ​ 为了解决一一映射页表体积过大问题采用了类似哈希的结构。 ​ 虚拟地址的前10位是页目录共计2^10个即1KB大小中间10位是页表每一个页目录指向一张页表每张页表大小1KB共有1KB张页表合计大小1MB后12位代表所属页表指向物理内存的偏移量加上这个偏移量即可找到真实的物理地址 我们平时写出这样的代码为什么程序会崩溃? char*shello; *sH; 解释: 字符串常量区是不允许被修改的只允许被读取— 为什么? — s里面保存的是指向的字符的虚拟起始地址 — s寻址的时候必定会伴随虚拟到物理的转化 — MMU 查页表的方式 — 对你的操作进行权限审查* — 发现你虽然能找到但是你进行的操作是非法的 — MMU会发生异常 — 异常转换成信号发送给目标进程 — 在从内核切换成为用户态的时候进行信号处理 — 会终止进程此程序发生崩溃。 深入理解现象: 我们实际在申请malloc内存时OS只要给你在虚拟地址空间上申请就可以了当你在真正访问空间(执行我自己代码)OS才会自动给你申请或者填充页表(缺页中断现象) 申请具体的物理内存 2. 线程控制 2.0 POSIX线程库 两种角度: 用户角度: 只认线程操作系统角度: Linux下没有真正的线程而是用进程模拟的线程(LWP) — 所以Linux不会提供直接创建线程的系统调用它会给我们最多提供创建轻量级进程的接口 POSIX线程库: 即用户级线程库对下将linux接口封装对上给用户提供进行线程控制的接口 任何系统都要自带它是一种原生线程库。 细节: 与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库要通过引入头文 pthread.h链接这些线程函数库时要使用编译器命令的“-lpthread”选项 使用Linux线程库记得在编译时添加 -lpthread 选项 ps -aL 查看当前操作系统中的线程 深入理解线程库 线程库也像前面我们学习的动静态库一样它的本质就是一个文件它从磁盘加载到物理内存通过页表映射到虚拟地址空间的共享区中我们进程中的线程可以进行一系列操作依赖的就是这个库文件换句话说我们可以随时访问库中的代码和数据。 那么我们如何管理库中的代码和数据呢先描述再组织创建类似的管理线程TCB。 类比文件系统中的struct FILE, 在语言层面上使用的是struct FILE底层使用的是struct file来管理这里底层使用LWP管理语言层面上使用TCB。不论是底层还是在语言层面两者都是在库中管理。 使用C多线程接口在Linux环境创建多线程 任何语言在Linux中使用多线程编程必须使用-pthread进行链接。 C的thread库底层有条件编译会判断当前的运行环境执行适用于Linux或windows的多线程代码。 在Linux环境中C的多线程本质就是对pthread库的封装。 #includeiostream #includeunistd.h #includecstring #includecstdio #includestring #includepthread.h #includethreadusing namespace std;void run1() {while(true){cout thread 1 endl;sleep(1);} } void run2() {while(true){cout thread 2 endl;sleep(1);} } void run3() {while(true){cout thread 3 endl;sleep(1);} } int main() {thread th1(run1);thread th2(run2);thread th3(run3);th1.join();th2.join();th3.join();return 0; }深入理解线程id 线程idpthread_t 就是一个地址数据用来标识线程相关属性集合的起始地址 线程有独立的栈结构: 所有线程都要有自己独立的栈结构主线程用的是进程系统栈新线程用的是库中提供的栈 我们写一段代码打印出线程的id来观察一下: 此时我们拿到的线程id其实是库中该线程对应属性集的起始地址类比上面我们讲过: 定义一个变量采用基地址偏移量的方式我们拿到这个变量的地址是它的起始地址(低地址)。 运行结果: 线程局部存储 这样的一段代码 运行结果: 我们观察发现3个线程中cnt的结果不同有一个疑问3个线程中cnt的地址应该一样吗? 不应该一样也绝对不一样因为3个线程用的是不同的栈cnt是被开辟在不同的栈当中的 如何理解cnt是被开辟在不同的栈当中的呢 cpu中存在ebp和esp两个寄存器只要更改ebp和esp就能切换线程的栈3个线程中的栈也就是这样切换的 还是上面的代码我们定义一个全局变量g_val, 在threadRoutine函数中对g_val并打印出g_val的值和地址 观察运行结果发现: 3个线程一起对g_val进行操作并且g_val的地址相同 现象: 全局变量在已初始化数据段开辟空间并不属于线程的私有数据所以被所有线程共享多个线程对全局变量做修改时他们的地址相同 我们在全局变量g_val前添加 __thread后发现现象又正常了g_val立马变成每个线程私有的了。 __thread: 构建每个线程的局部存储 发现地址比上面的大: 因为这次是映射到堆与栈之间所以地址变大了 2.1 线程的创建 - pthread_create PTHREAD_CREATE(3) #includepthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 参数1、thread输出型参数指向线程标识符的指针线程创建成功后将通过此指针返回线程标识符。2、attr:线程属性包括线程的栈大小、调度策略、优先级等信息。如果为NULL则使用默认属性。3、start_routine线程启动后要执行的函数指针。4、arg线程函数的参数将传递给线程函数的第一个参数。 返回值pthread_create()成功返回0。失败时返回错误号*thread中的内容是未定义的。运行结果: 我们可以看到: 同一个进程中的线程PID相同但是LWP不同。主线程的LWP等于PID 问题: 那么主线程和新线程哪个先运行呢不确定由调度器决定 线程创建时参数传一个对象 结合和后面线程控制的几个接口 #includeiostream #includepthread.h #includeunistd.h #includestring using namespace std;#define NUM 10enum {OK0,ERROR };class ThreadData { public:ThreadData(const stringname,int id,time_t createTime,int top):_name(name),_id(id),_createTime((uint64_t)createTime),_status(OK),_top(top),_result(0){}~ThreadData(){}public://输入的string _name;int _id;uint64_t _createTime;//返回的int _status;int _top;int _result; }; void*thread_run(void*args) {ThreadData*td static_castThreadData*(args);for(int i1;itd-_top;i){td-_resulti;}couttd-_name cal done endl;// pthread_exit(td); 这个或者下面的return都可以return td; //可以返回对象 }int main() {pthread_t tids[NUM];for(int i0;iNUM;i){char tname[64];snprintf(tname,64,thread-%d,i1);ThreadData*tdnew ThreadData(tname,i1,time(nullptr),100i*5);pthread_create(tidsi,nullptr,thread_run,td); //创建线程时可以传对象sleep(1);}void*retnullptr;for(int i0;iNUM;i){int npthread_join(tids[i],ret); //获取每个线程等待的结果if(n!0) cerrpthread_join errorendl;ThreadData*td static_castThreadData*(ret); //接收结果if(td-_statusOK){couttd-_name计算的结果是: td-_result (它要计算的是[1, td-_top ]) endl;}delete td;}coutall thread quit.... endl;return 0; }运行结果: 线程创建时参数传一个字符串 #includeiostream #includepthread.h #includeunistd.h using namespace std;#define NUM 10void*threadRun(void*args) {const char*namestatic_castconst char*(args);int cnt5;while(cnt){cout name is running: cnt--endl;sleep(1);}pthread_exit((void*)11); }int main() {pthread_t tid;pthread_create(tid,nullptr,threadRun,(void*)thread 1); //字符串是只读的, 所以要强转sleep(3);void*retnullptr;pthread_join(tid,ret);cout new thread exit : (int64_t)ret endl;return 0; }运行结果: 2.2 线程的等待 - pthread_join PTHREAD_JOIN(3) #includepthread.h int pthread_join(pthread_t thread, void **retval); thread要等哪一个线程 retval输出型参数用于获取线程函数返回时的退出结果(回调函数返回值是void*这里用void**接收这个返回值) 返回值在成功时pthread_join()返回0; 在错误时它返回一个错误码。为什么需要线程等待类比进程子进程退出父进程不回收就会出现僵尸状态。 已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。 创建10个新线程然后每个线程退出主线程进行线程等待获取每个线程的等待结果 #includeiostream #includepthread.h #includeunistd.h using namespace std;#define NUM 10void*thread_run(void*args) //thread_run方法被重入了 {char*name (char*)args;while(true){coutnew thread running, my thread name is: name endl;sleep(4);break;}delete name;return nullptr; } int main() {pthread_t tids[NUM];for(int i0;iNUM;i){//每次循环都是重新去new,每次都有新的堆空间,再去创建,地址肯定不相同,相当于给每一个线程创建了一份堆空间char*tnamenew char[64]; snprintf(tname,64,thread-%d,i1);pthread_create(tidsi,nullptr,thread_run,tname); //tname缓冲区是共享的, 传递的是缓冲区的起始地址}for(int i0;iNUM;i){int npthread_join(tids[i],nullptr); //获取每个线程等待的结果if(n!0) cerrpthread_join errorendl;}coutall thread quit....endl;return 0; }运行结果: 2.3 线程的终止 还是上面的代码如果在break前添加exit, 让这个线程退出会发生什么现象呢 运行结果: 我们发现只创建了几个新线程右边的监控脚本什么也没有这是为什么呢 exit是进程退出, 不是线程退出, 只要有任意一个进程调用exit, 整个进程(所有线程)全部退出! 还来不及等待线程呢整个进程已经全部退出了。 如果需要只终止某个线程而不终止整个进程,可以有三种方法: 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。 2.3.1 线程函数return 2.3.2 线程终止 - pthread_exit 功能线程终止 原型 void pthread_exit(void *value_ptr); 参数 value_ptr:value_ptr不要指向一个局部变量。 返回值无返回值跟进程一样线程结束的时候无法返回到它的调用者(自身)需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。 还是线程等待部分的代码在delete后面添加pthread_exit(nullptr); 运行结果: 也可以这样写: pthread_exit(void*) 可以获取线程退出的结果 运行结果: 2.3.3 线程取消 - pthread_cancel PTHREAD_CANCEL(3) #includepthread.h int pthread_cancel(pthread_t thread); thread要取消哪一个线程 返回值在成功时pthread_cancel ()返回0; 在出错时它返回一个非零的错误码。 一个线程被取消, 退出码: -1结合线程创建时参数传一个字符串部分的代码 运行结果: 2.4 获取线程自身的ID - pthread_self PTHREAD_SELF(3) #include pthread.h pthread_t pthread_self(void); 返回值此函数始终成功返回调用线程的ID。结合线程创建时参数传一个字符串部分的代码 运行结果: 2.5 线程的分离 - pthread_detach 默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候我们可以告诉系统当线程退出时自动释放线程资源。 PTHREAD_DETACH(3) #include pthread.h int pthread_detach(pthread_t thread); thread线程ID 返回值在成功时pthread_detach()返回0; 在错误时它返回一个错误码。可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离: pthread_detach(pthread_self());一个线程如果被分离就无法再被join, 如果join会报错 错误示例1: 一个线程不能既是joinable又是分离的。 运行结果: 此代码中新线程被创建出来新线程去执行打印新消息主线程继续向下执行主线程去join时发现新线程是线程分离的join立马失败直接出错返回打印错误消息立马return了主线程退出就是进程退出即进程中所有线程退出所以此时右边的监测什么也没有 错误示例2: 把pthread_detach放在新线程的执行函数里有可能发生主线程已经在join处开始等待了新线程才走到执行分离的代码等新线程执行完回调函数内的代码时主线程自然join等待成功了。这是错误写法。 运行结果: 仿佛像是主线程join成功 正确用法: 创建新线程成功时由主线程进行分离 void*threadRoutine(void* args) {string namestatic_castconst char*(args);sleep(5);return nullptr; } int main() {pthread_t tid;pthread_create(tid,nullptr, threadRoutine ,(void*)thread 1); pthread_detach(tid); //还是放在这里比较好sleep(1);return 0; }3. 线程互斥 3.1 举例 — 多线程中全局变量并发访问的问题 int g_val100; //__thread修饰的全局变量: 构建每个线程的局部存储void*threadRoutine(void* args) //这个函数是被重入的 {string namestatic_castconst char*(args);int cnt5; //3个线程每一个线程都有一个while(cnt){cout name g_val: g_val-- , g_val: g_val endl;sleep(1);}return nullptr; }int main() {pthread_t t1,t2,t3;pthread_create(t1,nullptr, threadRoutine ,(void*)thread 1); pthread_create(t2,nullptr, threadRoutine ,(void*)thread 2); pthread_create(t2,nullptr, threadRoutine ,(void*)thread 3); pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);return 0; }此代码运行结果和线程局部存储部分代码运行结果相似3个线程一起对g_val进行–操作并且g_val的地址相同 我们以两个线程threadA和threadB来模拟一下此过程 两个线程去访问g_val的数据因为时间片调度的关系threadB好不容易把g_val中的值修改成10了threadB时间片到了调度threadA时它又把g_val中的值改回99了 3.2 进程线程间的互斥相关背景概念 临界资源多线程执行流共享的资源就叫做临界资源临界区每个线程内部访问临界资源的代码就叫做临界区互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用原子性后面讨论如何实现不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成 在上面3.1的例子中: 3.3 举例 — 多线程并发抢票 #includeiostream #includeunistd.h #includecstring #includecstdio #includestring #includepthread.husing namespace std;int tickets10000; //临界资源void*threadRoutine(void*name) {string tnamestatic_castconst char*(name);while(true){if(tickets0) //临界区{usleep(2000); // 模拟抢票花费的时间, 单位是微秒cout tname get a ticket: tickets-- endl; //临界区}else{break;}}return nullptr; }int main() {pthread_t t[4];int nsizeof(t)/sizeof(t[0]);for(int i0;in;i){char*datanew char[64];snprintf(data,64, thread-%d, i1);pthread_create(ti,nullptr,threadRoutine,data);}for(int i0;in;i){pthread_join(t[i],nullptr);}return 0; }运行结果: 发现票直接被抢到了负数这就是出现了3.1中多线程并发抢票的问题 3.4 互斥量mutex 要解决以上问题需要做到三点 代码必须要有互斥行为当代码进入临界区执行时不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行那么只能允许一个线程进入该临界区。如果线程不在临界区中执行那么该线程不能阻止其他线程进入临界区。 要做到这三点本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。 3.4.1 互斥量的接口 初始化互斥量 初始化互斥量有两种方法 静态分配 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER动态分配 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t* restrict attr); 参数 mutex要初始化的互斥量 attrNULL销毁互斥量 销毁互斥量需要注意 使用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_ lock 时可能会遇到以下情况: 互斥量处于未锁状态该函数会将互斥量锁定同时返回成功发起函数调用时其他线程已经锁定互斥量或者存在其他线程同时申请互斥量但没有竞争到互斥量那么pthread_ lock调用会陷入阻塞(执行流被挂起)等待互斥量解锁。 3.4.2 互斥量实现原理 为了实现互斥锁操作,**大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,**即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 3.5 加锁后的多线程抢票 3.5.1 在3.1中的代码下加锁 #includeiostream #includeunistd.h #includecstring #includecstdio #includestring #includepthread.h #includepthread.husing namespace std;int tickets10000; //临界资源 pthread_mutex_t mutex; //锁void*threadRoutine(void*name) {string tnamestatic_castconst char*(name);while(true){pthread_mutex_lock(mutex); //所有线程都要遵守这个规则if(tickets0) //临界区{usleep(2000); // 模拟抢票花费的时间, 单位是微秒cout tname get a ticket: tickets-- endl; //临界区pthread_mutex_unlock(mutex);}else{pthread_mutex_unlock(mutex);break;}//后面还有动作usleep(1000); //充当抢完1张票, 后续动作}return nullptr; }int main() {pthread_mutex_init(mutex,nullptr); //初始化pthread_t t[4];int nsizeof(t)/sizeof(t[0]);for(int i0;in;i){char*datanew char[64];snprintf(data,64, thread-%d, i1);pthread_create(ti,nullptr,threadRoutine,data);}for(int i0;in;i){pthread_join(t[i],nullptr);}pthread_mutex_destroy(mutex); //销毁return 0; }运行结果: 加锁后发现抢票可以正常进行 3.5.2 优化加锁后的多线程抢票 在main函数中定义一把锁让锁可以被所有的线程看到 #includeiostream #includeunistd.h #includestring.h #includecstdio #includestring #includepthread.h using namespace std;// 细节: // 1. 凡是访问同一个临界资源的线程, 都要进行加锁保护, 而且必须加同一把锁, 这是一个游戏规则, 不能有例外 // 2. 每一个线程访问临界资源之前, 都得加锁, 加锁本质是 给临界区加锁, 加锁的粒度尽量要细一些 // 3. 线程访问临界区的时候, 需要先加锁 - 所有线程都必须要先看到同一把锁 - 锁本身就是公共资源 - 锁如何保证自己的安全? - // 加锁和解锁本身就是原子性的! // 4. 临界区可以是一行代码, 可以是一批代码, a.线程可能被切换吗 当然可能, 不要特殊化加锁和解锁, 还有临界区的代码 // b.切换会有影响吗? 不会, 因为在我不在期间, 任何人都没有办法进入临界区, 因为他无法成功的申请到锁! 因为锁被我拿走了 // 5. 这也正是体现互斥带来的串行化的表现, 站在其他线程的角度, // 对其他线程有意义的状态就是: 1. 锁被我申请(持有锁) 2. 锁被我释放了(不持有锁), 原子性就体现在这里 // 6. 解锁的过程也被设计成为原子的 // 7. 锁 的 原理的理解//临界资源 int tickets1000; //全局变量, 共享对象//线程创建传递的参数 --- 锁可以被所有线程看到 class TData { public:TData(const string name,pthread_mutex_t*mutex):_name(name),_pmutex(mutex){}~TData(){}public:string _name;pthread_mutex_t* _pmutex; //互斥锁对应的指针 };void*threadRoutine(void*args) {TData*tdstatic_castTData*(args);while(true){pthread_mutex_lock(td-_pmutex); //加锁, 是一个让不让你通过的策略if(tickets0){usleep(2000);couttd-_name get a ticket: tickets-- endl; //临界区pthread_mutex_unlock(td-_pmutex);}else{pthread_mutex_unlock(td-_pmutex);break;}// 我们抢完一张票的时候, 我们还要有后续的动作usleep(13);}return nullptr; }int main() {pthread_mutex_t mutex;pthread_mutex_init(mutex,nullptr);pthread_t tid[4];int nsizeof(tid)/sizeof(tid[0]);for(int i0;in;i){char name[64];snprintf(name,64,thread-%d, i1);TData*tdnew TData(name, mutex);pthread_create(tidi,nullptr,threadRoutine, td);}for(int i0;in;i){pthread_join(tid[i],nullptr);}pthread_mutex_destroy(mutex);return 0; }线程申请到锁后在临界区中被切换会有影响吗? 不会, 因为在我不在期间, 任何人都没有办法进入临界区, 因为他无法成功的申请到锁! 因为锁被我拿走了 类比举例: 在你的学校里有1间VIP自习室它是一个单间自习室只允许一人自习假如某一天你来的很早早早地进入了自习室中你为了防止别人进来拿到钥匙开门后将门从里面反锁并将钥匙装入自己的兜里此时外面陆陆续续来人了但是他们进不来 学习2小时后你突然想要去上厕所这时你从自习室里面走出来将门锁上把钥匙装进自己的兜里才去上厕所即使外面站了很多人在你上厕所这段时间里也无人能进入此间自习室因为钥匙被你带走了。 3.6 封装原生线程库和锁 3.6.1 封装原生线程库 #includeiostream #includestring using namespace std;class Thread { public:typedef enum{NEW0,RUNNING,EXITED}ThreadStatus;typedef void (*func_t)(void*); //函数指针, 参数是void*Thread(int num, func_t func, void*args):_tid(0),_status(NEW),_func(func),_args(args){char name[128];snprintf(name,sizeof(name),thread-%d,num);_namename;}int status() {return _status;}string threadname() {return _name;}pthread_t thread_id(){if(_statusRUNNING)return _tid;elsereturn 0;}// runHelper是不是类的成员函数, 而类的成员函数, 具有默认参数this, 需要static// void*runHelper(Thread*this, void*args) , 而pthread_create要求传的参数必须是: void*的, 即参数不匹配// 但是static会有新的问题: static成员函数, 无法直接访问类属性和其他成员函数static void*runHelper(void*args){Thread*ts(Thread*)args; //就拿到了当前对象// _func(_args);(*ts)();}//仿函数void operator()(){_func(_args);}void run(){int npthread_create(_tid,nullptr,runHelper,this); //this: 是当前线程对象Threadif(n!0) exit(-1);_statusRUNNING;}void join(){int npthread_join(_tid,nullptr);if(n!0){cerr main thread join thread _name error endl;}_statusEXITED;}~Thread(){}private:pthread_t _tid;string _name;func_t _func; //线程未来要执行的回调void*_args; //调用回调函数时的参数ThreadStatus _status; };3.6.2 封装锁 - RAII风格的锁 #includeiostream #includepthread.h using namespace std;class Mutex //自己不维护锁,由外部传入 { public:Mutex(pthread_mutex_t* mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex; //锁的指针 };class LockGuard //自己不维护锁,由外部传入 { public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}private:Mutex _mutex; //锁的指针 };3.6.3 使用自己的封装实现多线程抢票 #includeiostream #includeunistd.h #includestring.h #includecstdio #includestring #includepthread.h #includethread.hpp #includelockGuard.hpp using namespace std;//临界资源 int tickets1000; //全局变量, 共享对象 pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; //这是我在外部定义的锁void threadRoutine(void*args) {string message static_castconst char*(args);while(true){{ // 定义的临时对象, 可以自动完成加锁和解锁LockGuard lockguard(mutex); //RAII风格的锁if (tickets 0){usleep(2000);cout message get a ticket: tickets-- endl; // 临界区}else{break;}}// 我们抢完一张票的时候, 我们还要有后续的动作usleep(13);} }int main() {Thread t1(1, threadRoutine,(void*)helloyj1);Thread t2(2, threadRoutine,(void*)helloyj2);Thread t3(3, threadRoutine,(void*)helloyj3);Thread t4(4, threadRoutine,(void*)helloyj4);t1.run();t2.run();t3.run();t4.run();t1.join();t2.join();t3.join();t4.join();return 0; }运行结果: 多线程正常抢票 4. 可重入VS线程安全 4.1 概念 线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。 4.2 常见的线程不安全的情况 不保护共享变量的函数函数状态随着被调用状态发生变化的函数返回指向静态变量指针的函数调用线程不安全函数的函数 4.3 常见的线程安全的情况 每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性 4.4 常见不可重入的情况 调用了malloc/free函数因为malloc函数是用全局链表来管理堆的调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构可重入函数体内使用了静态的数据结构 4.5 常见可重入的情况 不使用全局变量或静态变量不使用用malloc或者new开辟出的空间不调用不可重入函数不返回静态或全局数据所有数据都有函数的调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据 4.6 可重入与线程安全联系 函数是可重入的那就是线程安全的函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。 4.7 可重入与线程安全区别 可重入函数是线程安全函数的一种线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。 5. 常见锁概念 学习逻辑: 多线程代码 并发访问临界资源 加锁 可能导致死锁 解决死锁问题 死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。 举例: 张三和李四是两个好朋友张三比较壮这天两个人手中分别持有5毛钱两人相约去商店买棒棒糖结果到了商店得知一根棒棒糖1元钱两人各自的钱都不够于是张三让李四将5毛钱给他让他凑成1元钱买糖李四不愿意同样李四也想拿张三手里的钱买糖张三也不愿意于是两人谁也不愿意给对方自己的钱但是却想要对方的钱两人就这样僵持下去。这样的状态就是一种死锁的状态。 一把锁也会出现死锁重复申请一把锁。 死锁四个必要条件 互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 避免死锁 核心思想: 破坏死锁的4个必要条件的任意一个 能不用锁就不要用锁主动释放锁按照顺序申请锁控制线程统一释放锁 验证: 一个线程申请锁一个线程释放锁 #includeiostream #includepthread.h #includeunistd.h using namespace std;pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER;void*threadRoutine(void*args) {coutI am a new threadendl;pthread_mutex_lock(mutex);coutI got a mutexendl;pthread_mutex_lock(mutex); //申请锁的问题, 它会停下来coutI alive againendl;return nullptr; } int main() {pthread_t tid;pthread_create(tid,nullptr, threadRoutine,nullptr);sleep(3);coutmain thread run beginendl;pthread_mutex_unlock(mutex);coutmain thread unlock...endl;sleep(3);return 0; }运行结果: 6. 线程同步 6.0 引入 自习室故事2.0版本 还是那个熟悉的自习室故事的前半段已经在 3.5.2 优化加锁后的多线程抢票 这部分讲过了下面进入故事的后半段。 有一天你来的很早早早地进入自习室自习。自习两小时后你突然很饿想起自己没吃早饭此时你打算离开自习室去吃饭这次你要带走自己的东西并且锁好门把钥匙挂在墙上后离开。但是当你刚出了自习室把钥匙挂在墙上后看到外面陆陆续续的人又后悔了想起自己吃完饭后可能得等很久才能进入自习室于是你后悔了不吃饭了因为你离墙近其他同学离墙远于是你快速拿起钥匙开门后又进入自习室自习你刚自习两分钟又饿了想离开去吃饭你又把钥匙去挂在墙上刚把钥匙挂在墙上你又后悔了又拿下钥匙进入自习室去自习了。因此你在这一天早晨重复最多的动作就是: 锁门挂钥匙拿钥匙开门。你自己没有好好自习其他同学也无法进入自习室自习。那么请问: 你错了吗 没错但是不合理。你频繁的锁门挂钥匙拿钥匙开门动作你自己没有实质性自习的同时其他同学也只能等你真正归还钥匙才能进入自习室自习。 校领导看到这样现象专门给此自习室增加了2条规矩: 1. 自习完毕的人归还完钥匙不能立即申请 2. 在外面等的人必须排队。 回到线程同步部分如果一个线程频繁的申请锁释放锁释放锁后又申请锁就会造成其他线程饥饿的问题。要在安全的规则下多线程访问资源具有一定的顺序性为了合理解决饥饿问题我们提出了线程同步让多线程进行协同工作。 6.1 同步概念与竞态条件 同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解。 6.2 条件变量 当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如一个线程访问队列时发现队列为空它只能等待只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。 6.2.1 条件变量的函数 初始化 初始化条件变量有两种方法 静态分配 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁 pthread_cond_t cond PTHREAD_COND_INITIALIZER;动态分配 int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数 cond要初始化的条件变量 attrNULL销毁 int pthread_cond_destroy(pthread_cond_t *cond)等待条件满足 int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数 cond要在这个条件变量上等待 mutex互斥量后面详细解释唤醒等待 int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有线程 int pthread_cond_signal(pthread_cond_t *cond); //唤醒一个线程6.2.2 条件变量使用示例 允许多线程在cond队列中队列式等待(就是一种顺序) #includeiostream #includepthread.h #includeunistd.h #includecstring #includestring using namespace std;const int num5;pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER;void*active(void*args) {string namestatic_castconst char*(args);while(true){pthread_mutex_lock(mutex);pthread_cond_wait(cond,mutex); // pthread_cond_wait, 调用的时候会自动释放锁, TODOcoutname 活动 endl;pthread_mutex_unlock(mutex);} } int main() {pthread_t tids[num];for(int i0;inum;i){char*namenew char[32];snprintf(name,32,thread-%d,i1);pthread_create(tidsi,nullptr,active,name);}sleep(3);// 唤醒等待while(true){coutmain thread wakeup thread... endl;// pthread_cond_signal(cond); //一个一个唤醒pthread_cond_broadcast(cond); //唤醒所有线程sleep(1);}for(int i0;inum;i){pthread_join(tids[i],nullptr);}return 0; }pthread_cond_broadcast(cond)运行结果: pthread_cond_signal(cond)运行结果: 7. 生产者消费者模型 7.0 引入 我们可以以去超市买东西为例来看待这个模型在超市买东西时其实是供货商向超市提供商品消费者去超市购买商品超市在这里是一种交易场所。以这个为例在生产者和消费者模型中生产者和消费者是两线程超市是一种特定的缓冲区 321原则 cp问题思路 深入理解 既然交易场所是一种特定的缓冲区那么: 交易场所必须先被所有线程看到(生产者和消费者线程)注定了交易场所一定会是一个被多线程并发访问的公共区域!注定了多线程一定要保护共享资源的安全注定了一定是在这种情况下要自己维护线程互斥与同步的关系 为何要使用生产者消费者模型 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。 生产者消费者模型优点 解耦 支持并发 支持忙闲不均 7.1 基于BlockingQueue的生产者消费者模型 BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于当队列为空时从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时往队列里存放元素的操作也会被阻塞直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞) 7.1.1 单生产单消费模型 (1)blockQueue.hpp #includeiostream #includepthread.h #includetime.h #includequeue #includeunistd.h #includestring using namespace std;const int gcap5;// 核心思想: // 队列为空消费者不应该再消费, 要等待, 后面有数据还要唤醒 // 队列为满生产者不应该再生产, 要等待, 后面有数据还要唤醒 // 所以两者都要有自己的条件变量// 不要认为, 阻塞队列只能放整数字符串之类的, 也可以放对象 templateclass T class BlockQueue { public:BlockQueue(const int capgcap):_cap(cap){pthread_mutex_init(mutex,nullptr);pthread_cond_init(_customerCond,nullptr);pthread_cond_init(_producerCond,nullptr);}bool isFull(){return _q.size()_cap;}bool isEmpty(){return _q.empty();}void push(const Tin){pthread_mutex_lock(mutex);// 细节1: 一定要保证, 在任何时候, 都是符合条件, 才进行生产while(isFull()) //1. 我们只能在临界区内部, 判断临界资源是否就绪! 注定了我们在当前一定是持有锁的!{//2. 要让线程休眠等待, 不能持有锁等待!//3. 注定了, pthread_cond_wait要有锁的释放的能力!pthread_cond_wait(_producerCond,mutex); //我休眠(切换)了, 我醒来的时候, 在哪里往后执行呢?//4. 当线程醒来的时候, 注定了继续从临界区内部继续运行! 因为我是在临界区被切走的!//5. 注定了当线程被唤醒的时候, 继续在pthread_cond_wait函数处向后运行, 又要重新申请锁, 申请成功才会彻底返回}// 没有满的, 要让他进行生产_q.push(in);// 加策略// if(_q.size() _cap/2) pthread_cond_signal(_customerCond); //唤醒, 要去唤醒对方pthread_mutex_unlock(mutex);//pthread_cond_signal(_customerCond); //也可以放在后面}void pop(T*out){pthread_mutex_lock(mutex);while(isEmpty()){pthread_cond_wait(_customerCond,mutex); //等待}*out_q.front();_q.pop();pthread_cond_signal(_producerCond); //唤醒pthread_mutex_unlock(mutex);}~BlockQueue(){pthread_mutex_destroy(mutex);pthread_cond_destroy(_customerCond);pthread_cond_destroy(_producerCond);}private:queueT _q;int _cap; //队列容量上限, 队列空满两种情况都要考虑// 为什么这份代码, 只用一把锁呢?根本原因在于, // 我们生产和消费访问的是同一个queue queue被当做整体使用pthread_mutex_t mutex; //保证数据安全pthread_cond_t _customerCond; //消费者对应的条件变量, 空, wait pthread_cond_t _producerCond; //生产者对应的条件变量, 满, wait};(2)main.cc(阻塞队列放整数) #includeblockQueue.hppvoid*customer(void*args) {BlockQueueint *bqstatic_castBlockQueueint*(args);while(true){//现象: 开始消费者,消费的慢, 生产者一瞬间把阻塞队列全部打满, 后面消费一个, 生产一个// sleep(1);// 1. 将数据从blockqueue中获取 --- 获取到了数据int data0;bq-pop(data);// 2. 结合某种业务逻辑处理数据 --- TODOcoutcustomer data: dataendl;} }void*producer(void*args) {BlockQueueint *bqstatic_castBlockQueueint*(args);while(true){sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int datarand()%101;// 2. 将数据推送到blockqueue中 --- 完成生产过程bq-push(data);coutproducer data: dataendl;} }int main() {// 单生产和单消费 --- 多生产和多消费srand((uint64_t)time(nullptr)^getpid());BlockQueueint *bqnew BlockQueueint();pthread_t c,p;// 让消费者和生产者看到同一个阻塞队列pthread_create(c,nullptr,customer,bq);pthread_create(p,nullptr,producer,bq);pthread_join(c,nullptr);pthread_join(p,nullptr);delete bq;return 0; }运行结果: (3)task.hpp #pragma once #includeiostream #includestringclass Task { public:Task(){}Task(int x,int y,char op):_x(x),_y(y),_op(op),_result(0),_exitCode(0){}void operator()(){switch(_op){case :_result_x_y;break;case -:_result_x-_y;break;case *:_result_x*_y;break;case /:{if(_y0)_exitCode-1;else_result_x/_y;}break;case %:{if(_y0)_exitCode-2;else_result_x%_y;}break;default:break;}}string formatArg(){return to_string(_x) _op to_string(_y) ;}string formatRes(){return to_string(_result) ( to_string(_exitCode) );}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode; };(4)main1.cc(阻塞队列放对象[任务]) #includeblockQueue.hpp #includetask.hppvoid*customer(void*args) {BlockQueueTask *bqstatic_castBlockQueueTask*(args);while(true){Task t; //调用它的无参构造函数// sleep(1); //现象: 开始消费者,消费的慢, 生产者一瞬间把阻塞队列全部打满, 后面消费一个, 生产一个// 1. 将数据从blockqueue中获取 --- 获取到了数据bq-pop(t);t(); //调用operator()仿函数, 处理任务// 2. 结合某种业务逻辑处理数据 --- TODOcoutcustomer data: t.formatArg()t.formatRes()endl;} }void*producer(void*args) {BlockQueueTask *bqstatic_castBlockQueueTask*(args);string opers-*/%;while(true){sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int xrand()%201;int yrand()%101;char opopers[rand() % opers.size()];Task t(x,y,op);// 2. 将数据推送到blockqueue中 --- 完成生产过程bq-push(t);coutproducer Task: t.formatArg()?endl;} }int main() {// 单生产和单消费 --- 多生产和多消费srand((uint64_t)time(nullptr)^getpid());BlockQueueTask *bqnew BlockQueueTask();pthread_t c,p;// 让消费者和生产者看到同一个阻塞队列pthread_create(c,nullptr,customer,bq);pthread_create(p,nullptr,producer,bq);pthread_join(c,nullptr);pthread_join(p,nullptr);delete bq;return 0; }运行结果: 7.1.2 多生产多消费模型 blockQueue.hpp和task.hpp跟上面单生产单消费模型代码相同 main2.cc #includeblockQueue.hpp #includetask.hppvoid*customer(void*args) {BlockQueueTask *bqstatic_castBlockQueueTask*(args);while(true){Task t; //调用它的无参构造函数sleep(1); //现象: 开始消费者,消费的慢, 生产者一瞬间把阻塞队列全部打满, 后面消费一个, 生产一个// 1. 将数据从blockqueue中获取 --- 获取到了数据bq-pop(t);t(); //调用operator()仿函数, 处理任务// 2. 结合某种业务逻辑处理数据 --- TODOcoutpthread_self() | customer data: t.formatArg()t.formatRes()endl;} }void*producer(void*args) {BlockQueueTask *bqstatic_castBlockQueueTask*(args);string opers-*/%;while(true){sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int xrand()%201;int yrand()%101;char opopers[rand() % opers.size()];Task t(x,y,op);// 2. 将数据推送到blockqueue中 --- 完成生产过程bq-push(t);coutpthread_self() | producer Task: t.formatArg()?endl;} }int main() {//多生产和多消费srand((uint64_t)time(nullptr) ^ getpid());BlockQueueTask *bqnew BlockQueueTask();pthread_t c[2],p[3];// 让消费者和生产者看到同一个阻塞队列pthread_create(c[0],nullptr,customer,bq);pthread_create(c[1],nullptr,customer,bq);pthread_create(p[0],nullptr,producer,bq);pthread_create(p[1],nullptr,producer,bq);pthread_create(p[2],nullptr,producer,bq);pthread_join(c[0],nullptr);pthread_join(c[1],nullptr);pthread_join(p[0],nullptr);pthread_join(p[1],nullptr);pthread_join(p[2],nullptr);delete bq;return 0; }运行结果: 7.2 生产者消费者模型真的高效吗 大量的生产者、消费者全部在争夺同一把锁也就是说一次只能放一个线程去阻塞队列中完成任务那效率不是非常慢不是的 因为传统的线程运作方式会让大部分线程阻塞在临界区之外而生产者消费者模型则是将任务的工序拆开一组线程分为生产者另一组分为消费者。充分利用了生产者的阻塞时间用以提前准备好生产资源同时也利用了消费者计算耗时的问题让消费者线程将更多的时间花在计算上而不是抢不到锁造成线程“干等”。 生产者消费者模型可以在生产前和消费后让线程并行执行减少线程阻塞时间。 8. POSIX信号量 8.1 概念理解 在之前进程间通信博客中我们感性地认识了信号量。今天这部分内容是对信号量更深的理解便于后续用信号量来实现基于环形队列的生产消费者模型。 以前: 信号量(信号灯): 本质就是一个计数器, 信号量需要进行PV操作P --, V , 是原子的二元信号量 互斥锁 今天: 信号量是描述临界资源中资源数目的。 每一个线程在访问对应资源的时候先申请信号量申请成功表示该线程允许使该资源申请不成功目前无法使用该资源 信号量的工作机制信号量机制类似于我们看电影买票是一种资源的预订机制 信号量已经是资源的计数器了申请信号量成功本身就表明资源可用申请信号量失败本身表明资源不可用 — 本质就是把判断转化成为信号量的申请行为 8.2 接口介绍 8.2.1 初始化信号量 #include semaphore.h int sem_init(sem_t *sem, int pshared, unsigned int value); 参数 pshared:0表示线程间共享非零表示进程间共享 value信号量初始值8.2.2 销毁信号量 int sem_destroy(sem_t *sem);8.2.3 等待信号量(P操作–) 功能等待信号量会将信号量的值减1 int sem_wait(sem_t *sem); //P()8.2.4 发布信号量(V操作) 功能发布信号量表示资源使用完毕可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V()8.3 基于环形队列的生产消费者模型 8.3.1 准备工作 (1)环形队列介绍 环形队列采用数组模拟用模运算来模拟环状特性。多预留一个空的位置作为满或空的状态 (2)构建cp问题 生产者和消费者关心的资源, 是一样的吗 不一样生产者关心空间消费者关心数据 只要信号量不为0表示资源可用表示线程可以访问 环形队列只要我们访问不同的区域生产和消费行为可以同时进行吗可以 生产者和消费者什么时候会访问同一个区域 他们两个刚开始队列中没有数据的时候(空) — 此时指向同一个位置存在竞争关系生产者先运行队列中全都是数据时(满) — 消费者先运行 综上: 只有为空和为满的时候cp才会指向同一个位置其他情况cp可以并发运行我们要保证整体规则同时也要保证为空或为满的时候的策略问题: (1) 队列为空生产者先运行 (2) 队列为满消费者先运行 (3) 不能让生产者套圈消费者 (4) 不能让消费者超过生产者 8.3.2 代码 (1) 单生产单消费模型 1 RingQueue.hpp #includeiostream #includepthread.h #includevector #includetime.h #includesys/types.h #includeunistd.h #includesemaphore.h using namespace std;// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源 static const int N5;templateclass T class RingQueue { private:void P(sem_t s){sem_wait(s);}void V(sem_t s){sem_post(s);}public:RingQueue(int numN):_ring(num),_cap(num){sem_init(_data_sem,0,0);sem_init(_space_sem,0,num);_c_step_p_step0;}void push(const Tin) // 对应生产者{// 生产 --- 先要申请信号量// 信号量申请成功 - 则一定能访问临界资源P(_space_sem);// 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢_ring[_p_step]in;_p_step%_cap;V(_data_sem);}void pop(T*out) // 对应消费者{// 消费P(_data_sem);*out_ring[_c_step];_c_step%_cap;V(_space_sem);}~RingQueue(){sem_destroy(_data_sem);sem_destroy(_space_sem);} private:vectorT _ring;int _cap; // 环形队列容器大小sem_t _data_sem; // 只有消费者关心sem_t _space_sem; // 只有生产者关心int _c_step; // 消费位置int _p_step; // 生产位置 };2 main.cc(环形队列放整数) #includeringQueue.hpp// 生产者不断生产, 消费者不断消费void*consumerRoutine(void*args) {RingQueueint*rqstatic_castRingQueueint*(args);while(true){// sleep(1); //现象: 开始消费者,消费的慢, 生产者一瞬间把环形队列全部打满, 后面消费一个, 生产一个// 1. 将数据RingQueue从中获取 --- 获取到了数据int data0;rq-pop(data);// 2. 结合某种业务逻辑处理数据 --- TODOcout consumer done: data endl;} } void*producerRoutine(void*args) {RingQueueint*rqstatic_castRingQueueint*(args);while(true){// sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int data rand() % 10 1;// 2. 将数据推送到RingQueue中 --- 完成生产过程rq-push(data);cout producer done: data endl;} }运行结果: 下面加上了sleep便于观察运行结果去掉这2个sleep就可以看到并发场景 3 task.hpp #pragma once #includeiostream #includestring #includeunistd.h #includestring.h using namespace std;class Task { public:Task(){}Task(int x,int y,char op):_x(x),_y(y),_op(op),_result(0),_exitCode(0){}void operator()(){switch(_op){case :_result_x_y;break;case -:_result_x-_y;break;case *:_result_x*_y;break;case /:{if(_y0)_exitCode-1;else_result_x/_y;}break;case %:{if(_y0)_exitCode-2;else_result_x%_y;}break;default:break;}usleep(10000); // 这是处理任务需要的时间}string formatArg(){return to_string(_x) _op to_string(_y) ?;}string formatRes(){return to_string(_result) ( to_string(_exitCode) );}~Task(){} private:int _x;int _y;char _op;int _result;int _exitCode; };4 main1.cc(环形队列放任务) #includeringQueue.hpp #includetask.hpp// 生产者不断生产, 消费者不断消费string opers-*/%;void*consumerRoutine(void*args) {RingQueueTask*rqstatic_castRingQueueTask*(args);while(true){// sleep(1); //现象: 开始消费者,消费的慢, 生产者一瞬间把环形队列全部打满, 后面消费一个, 生产一个// 1. 将数据RingQueue从中获取 --- 获取到了数据Task t;rq-pop(t); //把任务从共享区拿到自己的私有上下文// 2. 结合某种业务逻辑处理数据 --- TODOt(); // --- 仿函数处理任务cout consumer done, 处理完成的任务是: t.formatRes() endl;} }void*producerRoutine(void*args) {RingQueueTask*rqstatic_castRingQueueTask*(args);while(true){sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int x rand() % 100;int y rand() % 100;// 2. 将数据推送到RingQueue中 --- 完成生产过程char opopers[(xy)%opers.size()];Task t(x,y,op);rq-push(t);cout producer done, 生产的任务是: t.formatArg() endl;} }int main() {srand(time(nullptr)^getpid());RingQueueTask*rqnew RingQueueTask();// 单生产单消费pthread_t c,p;// 让生产者和消费者看到同一个环形队列pthread_create(c,nullptr,consumerRoutine,rq);pthread_create(p,nullptr,producerRoutine,rq);pthread_join(c,nullptr);pthread_join(p,nullptr);delete rq;return 0; }运行结果: (2) 多生产多消费模型 0 分析 在多生产多消费的模型下需要加锁吗需要 多生产多消费即创建多线程来完成多生产多消费模型下生产者和消费者对应的生产坐标和消费坐标只有一个即便是多生产多消费在进入环形队列时只能一个生产者进入一个消费者进入即存在互斥关系注定要加锁。 那么我们是先申请锁还是先申请信号量呢先申请信号量(即先分配资源) 先申请锁: 即当前线程持有锁期间其他线程只能在外部进行等待 先申请信号量: 即使某个线程持有锁其他线程也可以进行资源的分配 1 RingQueue.hpp #includeiostream #includepthread.h #includevector #includetime.h #includesys/types.h #includeunistd.h #includesemaphore.h #includemutex using namespace std;// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源 static const int N5;templateclass T class RingQueue { private:void P(sem_t s){sem_wait(s);}void V(sem_t s){sem_post(s);}void Lock(pthread_mutex_t m){pthread_mutex_lock(m);}void Unlock(pthread_mutex_t m){pthread_mutex_unlock(m);}public:RingQueue(int numN):_ring(num),_cap(num){sem_init(_data_sem,0,0);sem_init(_space_sem,0,num);_c_step_p_step0;pthread_mutex_init(_c_mutex,nullptr);pthread_mutex_init(_p_mutex,nullptr);}void push(const Tin) // 对应生产者{// 1.信号量的好处:// 可以不用在临界区内部做判断, 就可以知道临界资源的使用情况// 2.什么时候用锁, 什么时候用sem? --- 你对应的临界资源, 是否被整体使用!// 生产 --- 先要申请信号量// 信号量申请成功 - 则一定能访问临界资源P(_space_sem);Lock(_p_mutex);// 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢_ring[_p_step]in;_p_step%_cap;V(_data_sem);Unlock(_p_mutex);}void pop(T*out) // 对应消费者{// 消费P(_data_sem); // 1. 先申请信号量是为了更高效Lock(_c_mutex); // 2. *out_ring[_c_step];_c_step%_cap;V(_space_sem);Unlock(_c_mutex);}~RingQueue(){sem_destroy(_data_sem);sem_destroy(_space_sem);pthread_mutex_destroy(_c_mutex);pthread_mutex_destroy(_p_mutex);} private:vectorT _ring;int _cap; // 环形队列容器大小sem_t _data_sem; // 只有消费者关心sem_t _space_sem; // 只有生产者关心int _c_step; // 消费位置int _p_step; // 生产位置pthread_mutex_t _c_mutex;pthread_mutex_t _p_mutex; };2 main2.cc #includeringQueue1.hpp #includetask.hpp// 生产者不断生产, 消费者不断消费string opers-*/%;void*consumerRoutine(void*args) {RingQueueTask*rqstatic_castRingQueueTask*(args);while(true){// sleep(1); //现象: 开始消费者,消费的慢, 生产者一瞬间把环形队列全部打满, 后面消费一个, 生产一个// 1. 将数据RingQueue从中获取 --- 获取到了数据Task t;rq-pop(t); //把任务从共享区拿到自己的私有上下文// 2. 结合某种业务逻辑处理数据 --- TODOt(); // --- 仿函数处理任务cout consumer done, 处理完成的任务是: t.formatRes() endl;} } void*producerRoutine(void*args) {RingQueueTask*rqstatic_castRingQueueTask*(args);while(true){sleep(1); //现象: 生产一个, 消费一个// 1. 先通过某种渠道获取数据int x rand() % 100;int y rand() % 100;// 2. 将数据推送到RingQueue中 --- 完成生产过程char opopers[(xy)%opers.size()];Task t(x,y,op);rq-push(t);cout producer done, 生产的任务是: t.formatArg() endl;} } int main() {srand(time(nullptr)^getpid());RingQueueTask*rqnew RingQueueTask();// 多生产多消费// 意义在哪里呢?意义绝对不在从缓冲区中放入和拿取, 意义在于, 放前并发构建Task, 获取后多线程可以并发处理Task,// 因为这些操作没有加锁pthread_t c[3],p[2];// 让生产者和消费者看到同一个环形队列for(int i0;i3;i)pthread_create(ci,nullptr,consumerRoutine,rq);for(int i0;i2;i)pthread_create(pi,nullptr,producerRoutine,rq);for(int i0;i3;i)pthread_join(c[i],nullptr);for(int i0;i2;i)pthread_join(p[i],nullptr);delete rq;return 0; }运行结果: 9. 线程池 9.1 线程池介绍 线程池: 一种线程使用模式。线程过多会带来调度开销进而影响缓存局部性和整体性能。而线程池维护着多个线程等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 线程池的应用场景 需要大量的线程来完成任务且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务使用线程池技术是非常合适的。因为单个任务小而任务数量巨大你可以想象一个热门网站的点击次数。 但对于长时间的任务比如一个Telnet连接请求线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。对性能要求苛刻的应用比如要求服务器迅速响应客户请求。接受突发性的大量请求但不至于使服务器因此产生大量线程的应用。突发性大量客户请求在没有线程池情况下将产生大量线程虽然理论上大部分操作系统线程数目最大值不是问题短时间内产生大量线程可能使内存到达极限出现错误。 线程池示例 创建固定数量线程池循环从任务队列中获取任务对象获取到任务对象后执行任务对象中的任务接口 9.2 线程池代码 9.2.1 v1版本 利用linux原生线程库实现一个简单的线程池, task.hpp的代码见8.3.2 (1)threadPool_v1.hpp #includeiostream #includememory #includepthread.h #includevector #includequeue #includeunistd.h using namespace std;const static int N5; templateclass T class threadPool { public:void lockQueue(){pthread_mutex_lock(_lock);}void unlockQueue(){pthread_mutex_unlock(_lock);}void threadWait(){pthread_cond_wait(_cond,_lock);}void threadWakeup(){pthread_cond_signal(_cond); // 唤醒在条件变量下等待的线程}bool isEmpty(){return _tasks.empty();}T popTask(){T t_tasks.front();_tasks.pop();return t;}// 参数和返回值都是 void*, 但是类内成员函数存在this指针, 所以要添加static// 但是静态方法无法使用类内的非静态成员, 即需要传入this指针static void*threadRoutine(void*args) {// 线程分离, 让主线程不要关心他们pthread_detach(pthread_self());threadPoolT*tpstatic_castthreadPoolT*(args);while(true){// 1. 检测有没有任务 --- 本质是看队列是否为空 // --- 本质就是在访问共享资源 --- 必定加锁// 2. 有: 处理// 3. 无: 等待// 细节: 必定加锁tp-lockQueue();while(tp-isEmpty()){// 等待, 在条件变量下等待tp-threadWait();}T ttp-popTask(); // 把任务从公共区域拿到私有区域tp-unlockQueue();// for test// 处理任务应不应该在临界区中处理, 不应该, 这是线程自己私有的事情t(); coutthread handler done, result: t.formatRes()endl; }}threadPool(int numN):_num(num),_threads(num){pthread_mutex_init(_lock,nullptr);pthread_cond_init(_cond,nullptr);}void init(){// TODO}void start(){for(int i0;i_num;i){pthread_create(_threads[i],nullptr,threadRoutine, this);}}void pushTask(const Tt){lockQueue();_tasks.push(t);threadWakeup();unlockQueue();}~threadPool(){pthread_mutex_destroy(_lock);pthread_cond_destroy(_cond);}private:vectorpthread_t _threads; // 表示一组线程int _num; // 表示线程数量queueT _tasks; // 表示一批任务 --- 使用stl自动扩容的特性pthread_mutex_t _lock;pthread_cond_t _cond; };(2)main1.cc #includethreadPool_v1.hpp #includetask.hppint main() {unique_ptrthreadPoolTask tp(new threadPoolTask);// unique_ptrthreadPoolTask tp(new threadPoolTask(calback()));tp-init();tp-start();// 充当生产者, 比如从网络中读取数据, 构建成任务, 推送给线程池while(true){int x,y;char op;coutplease Enter x;cinx;coutplease Enter y;ciny; coutplease Enter op(-*/%);cinop;Task t(x,y,op);tp-pushTask(t);} }运行结果: 9.2.2 v2版本 在v1的基础上利用自己封装的thread.hpp来实现代码见3.6.1 (1)threadPool_v2.hpp #includeiostream #includememory #includevector #includequeue #includeunistd.h #includethread.hpp using namespace std;const static int N5; templateclass T class threadPool { public:void lockQueue(){pthread_mutex_lock(_lock);}void unlockQueue(){pthread_mutex_unlock(_lock);}void threadWait(){pthread_cond_wait(_cond,_lock);}void threadWakeup(){pthread_cond_signal(_cond); // 唤醒在条件变量下等待的线程}bool isEmpty(){return _tasks.empty();}T popTask(){T t_tasks.front();_tasks.pop();return t;}static void threadRoutine(void*args) {threadPoolT*tpstatic_castthreadPoolT*(args);while(true){// 1. 检测有没有任务 --- 本质是看队列是否为空 // --- 本质就是在访问共享资源 --- 必定加锁// 2. 有: 处理// 3. 无: 等待// 细节: 必定加锁tp-lockQueue();while(tp-isEmpty()){// 等待, 在条件变量下等待tp-threadWait();}T ttp-popTask(); // 把任务从公共区域拿到私有区域tp-unlockQueue();// for test// 处理任务应不应该在临界区中处理, 不应该, 这是线程自己私有的事情t(); coutthread handler done, result: t.formatRes()endl; }}threadPool(int numN):_num(num){pthread_mutex_init(_lock,nullptr);pthread_cond_init(_cond,nullptr);}void init(){for(int i0;i_num;i){_threads.push_back(Thread(i,threadRoutine,this));}}void check(){for(autot:_threads){coutt.threadname() running...endl;}}void start(){for(autot:_threads){t.run();}}void pushTask(const Tt){lockQueue();_tasks.push(t);threadWakeup();unlockQueue();}~threadPool(){for(autot:_threads){t.join();}pthread_mutex_destroy(_lock);pthread_cond_destroy(_cond);} private:vectorThread _threads; // 表示一组线程int _num; // 表示线程数量queueT _tasks; // 表示一批任务 --- 使用stl自动扩容的特性pthread_mutex_t _lock;pthread_cond_t _cond; };(2)main2.cc #includethreadPool_v2.hpp #includetask.hppint main() {unique_ptrthreadPoolTask tp(new threadPoolTask);// unique_ptrthreadPoolTask tp(new threadPoolTask(calback()));tp-init();tp-start();// 充当生产者, 比如从网络中读取数据, 构建成任务, 推送给线程池while(true){int x,y;char op;coutplease Enter x;cinx;coutplease Enter y;ciny; coutplease Enter op(-*/%);cinop;Task t(x,y,op);tp-pushTask(t);} }运行结果: 9.2.3 v3版本 v3版本在v2版本的基础上利用RAll风格的锁来实现线程池,thread.hpp和lockGuard.hpp的代码看前面的3.6.1和3.6.2。 (1)threadPool_v3.hpp #includeiostream #includememory #includevector #includequeue #includeunistd.h #includethread.hpp #includelockGuard.hpp using namespace std;const static int N5; templateclass T class threadPool { public:pthread_mutex_t* getlock(){return _lock;}void threadWait(){pthread_cond_wait(_cond,_lock);}void threadWakeup(){pthread_cond_signal(_cond); // 唤醒在条件变量下等待的线程}bool isEmpty(){return _tasks.empty();}T popTask(){T t_tasks.front();_tasks.pop();return t;}static void threadRoutine(void*args) {threadPoolT*tpstatic_castthreadPoolT*(args);while(true){// 1. 检测有没有任务 --- 本质是看队列是否为空// --- 本质就是在访问共享资源 --- 必定加锁// 2. 有: 处理// 3. 无: 等待// 细节: 必定加锁T t;{LockGuard lockguard(tp-getlock());while (tp-isEmpty()){// 等待, 在条件变量下等待tp-threadWait();}t tp-popTask(); // 把任务从公共区域拿到私有区域}// for test// 处理任务应不应该在临界区中处理, 不应该, 这是线程自己私有的事情t(); coutthread handler done, result: t.formatRes()endl; }}threadPool(int numN):_num(num){pthread_mutex_init(_lock,nullptr);pthread_cond_init(_cond,nullptr);}void init(){for(int i0;i_num;i){_threads.push_back(Thread(i,threadRoutine,this));}}void check(){for(autot:_threads){coutt.threadname() running...endl;}}void start(){for(autot:_threads){t.run();}}void pushTask(const Tt){LockGuard lockguard(_lock);_tasks.push(t);threadWakeup();}~threadPool(){for(autot:_threads){t.join();}pthread_mutex_destroy(_lock);pthread_cond_destroy(_cond);} private:vectorThread _threads; // 表示一组线程int _num; // 表示线程数量queueT _tasks; // 表示一批任务 --- 使用stl自动扩容的特性pthread_mutex_t _lock;pthread_cond_t _cond; };(2)main3.cc #includethreadPool_v3.hpp #includetask.hppint main() {unique_ptrthreadPoolTask tp(new threadPoolTask);tp-init();tp-start();tp-check();// 充当生产者, 比如从网络中读取数据, 构建成任务, 推送给线程池while(true){int x,y;char op;coutplease Enter x;cinx;coutplease Enter y;ciny; coutplease Enter op(-*/%);cinop;Task t(x,y,op);tp-pushTask(t);} }运行结果: 9.2.4 v4版本(基于懒汉方式实现单例模式的线程池) (0)单例模式介绍 单例模式的详细介绍请看: C博客中智能指针篇 某些类, 只应该具有一个对象(实例), 就称之为单例。 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。 饿汉实现方式和懒汉实现方式 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。 懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度。 v4版本是在v3版本的基础上完成了基于懒汉方式实现单例模式的线程池 (1)threadPool_v4.hpp 注意事项: 加锁解锁的位置双重 if 判定, 避免不必要的锁竞争 #includeiostream #includememory #includevector #includequeue #includeunistd.h #includethread.hpp #includelockGuard.hpp using namespace std;const static int N5;templateclass T class threadPool { public:pthread_mutex_t* getlock(){return _lock;}void threadWait(){pthread_cond_wait(_cond,_lock);}void threadWakeup(){pthread_cond_signal(_cond); // 唤醒在条件变量下等待的线程}bool isEmpty(){return _tasks.empty();}T popTask(){T t_tasks.front();_tasks.pop();return t;}static void threadRoutine(void*args) {threadPoolT*tpstatic_castthreadPoolT*(args);while(true){// 1. 检测有没有任务 --- 本质是看队列是否为空// --- 本质就是在访问共享资源 --- 必定加锁// 2. 有: 处理// 3. 无: 等待// 细节: 必定加锁T t;{LockGuard lockguard(tp-getlock());while (tp-isEmpty()){// 等待, 在条件变量下等待tp-threadWait();}t tp-popTask(); // 把任务从公共区域拿到私有区域}// for test// 处理任务应不应该在临界区中处理, 不应该, 这是线程自己私有的事情t(); coutthread handler done, result: t.formatRes()endl; }}static threadPoolT * getinstance(){if (instance nullptr) // 为什么要这样? 提高效率, 减少加锁的次数{LockGuard lockguard(instance_lock);if (instance nullptr){instance new threadPoolT();instance-init();instance-start();}}return instance;}void init(){for(int i0;i_num;i){_threads.push_back(Thread(i,threadRoutine,this));}}void check(){for(autot:_threads){coutt.threadname() running...endl;}}void start(){for(autot:_threads){t.run();}}void pushTask(const Tt){LockGuard lockguard(_lock);_tasks.push(t);threadWakeup();}~threadPool(){for(autot:_threads){t.join();}pthread_mutex_destroy(_lock);pthread_cond_destroy(_cond);}private:threadPool(int numN):_num(num){pthread_mutex_init(_lock,nullptr);pthread_cond_init(_cond,nullptr);}threadPool(const threadPoolTtp)delete;void operator(const threadPoolTtp)delete; private:vectorThread _threads; int _num; queueT _tasks; pthread_mutex_t _lock;pthread_cond_t _cond;static threadPoolT*instance;static pthread_mutex_t instance_lock; };templateclass T threadPoolT * threadPoolT::instancenullptr;templateclass T pthread_mutex_t threadPoolT::instance_lockPTHREAD_MUTEX_INITIALIZER;(2)main4.cc #includethreadPool_v4.hpp #includetask.hppint main() {// threadPoolTask::getinstance();printf(0x%x\n,threadPoolTask::getinstance());printf(0x%x\n,threadPoolTask::getinstance());printf(0x%x\n,threadPoolTask::getinstance());printf(0x%x\n,threadPoolTask::getinstance());printf(0x%x\n,threadPoolTask::getinstance());// 充当生产者, 比如从网络中读取数据, 构建成任务, 推送给线程池while(true){int x,y;char op;coutplease Enter x;cinx;coutplease Enter y;ciny; coutplease Enter op(-*/%);cinop;Task t(x,y,op);threadPoolTask::getinstance()-pushTask(t); //单例对象有可能在多线程场景中使用} }运行结果: 10. STL,智能指针和线程安全 10.1 STL中的容器是否是线程安全的? 不是。 原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。 10.2 智能指针是否是线程安全的? 对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。 11. 其他常见的各种锁(了解) 悲观锁在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起。乐观锁每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。CAS操作当需要更新数据时判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败失败则重试一般是一个自旋的过程即不断重试。自旋锁公平锁非公平锁 自旋锁 关于自旋锁和挂起等待锁的选择根据具体应用场景 PTHREAD_SPIN_LOCK(3P) #include pthread.h int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock);13. 读者写者问题 读写锁 在编写多线程的时候有一种情况是十分常见的。那就是有些公共数据修改的机会比较少。相比较改写它们读的机会反而高的多。通常而言在读的过程中往往伴随着查找的操作中间耗时很长。给这种代码段加锁会极大地降低我们程序的效率。那么有没有一种方法可以专门处理这种多读少写的情况呢 有那就是读写锁。 ​ 读写锁行为 当前所锁态读锁行为写锁请求无锁可以可以读锁可以阻塞写锁阻塞阻塞 读者写者问题就类似于我们以前在学校里面画的黑板报 画黑板报的同学是写者黑板报是共享的缓冲区称为交易场所看黑板报的同学称为读者 这里先限制一下: 画黑板报的同学在同一时刻只能由一名同学画 角色之间的关系分析 一个读者或多个读者 一个线程或多个线程 一个写者或多个写者 一个线程或多个线程 黑板报: 共享资源 写者与写者之间互斥关系(竞争画黑板报)读者与读者之间没有关系(你读你的我读我的, 互不影响(共同读同一块黑板报不存在一个个排队看黑板报))写者与读者之间互斥 同步互斥写者在画黑板报黑板报还未画完未写完数据此时不允许读者来看黑板报此时数据不完全如果进行读取只会读到残缺的数据。当读者在读黑板报不允许写者进行画黑板报写数据要不然读者数据都没读完就被你写者新写的数据给覆盖掉了。同步写者写完数据就要等待读者读取完数据读者读完数据了就要等待写者重新进行写入新的数据。) 321原则 3种关系 1、写者和写者: 互斥关系2、读者和读者: 没有关系谁都可以读3、读者和写者: 互斥关系与同步关系。 2种角色: 读者和写者 1个交易场所: 通常指的是内存中的一段共享缓冲区 为什么cp问题中消费者之间是互斥关系而rw问题中读者之间没有关系 cp问题中消费者会拿走数据但是读者不会 读写锁的接口 定义一个读写锁 pthread_rwlock_t xxx初始化读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t*restrict attr);销毁读写锁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);加锁和解锁 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读锁加锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写锁加锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁共用设置读写优先 int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref); /* pref 共有 3 种选择 PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先可能会导致写者饥饿情况PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先目前有 BUG导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP 一致PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先但写者不能递归加锁 */
http://www.zqtcl.cn/news/982040/

相关文章:

  • 手机端网站欣赏wordpress 文章rss
  • 做网站一定要实名认证吗国外免费空间网站申请
  • 阿里云网站空间主机长春网站建设设计
  • 龙华网站建设yihekj长沙招聘网站制作
  • 网站怎么做文本跳出来网络规划设计师有用吗
  • 室内设计网站官网大全中国那些企业做网站做得好
  • 状态管理名词解释网站开发网络营销推广方案案例
  • 做网站需要几大模板河南中国建设信息网
  • 成都温江网站建设空间网页版
  • 做美股的数据网站邢台网站建设公司哪家好一点
  • 青岛即墨网站开发查询建设用地规划许可证在哪个网站
  • 成都APP,微网站开发芜湖企业100强
  • 江门搜索引擎网站推广网约车多少钱一辆
  • 北京高端网站建设宣传请人做软件开发的网站
  • h网站建设长沙本地公众号
  • 苏州工业园区劳动局网站做不了合同建域名做网站
  • 内蒙古建设兵团网站组建网站开发团队
  • 劳务派遣做网站的好处广州最新新闻事件
  • 海兴网站建设公司网站建设原则
  • 网站建设完不管了自己怎么接手wordpress个人主页
  • 具有品牌的网站建设霞浦建设局网站
  • 推荐个网站免费的wordpress force ssl
  • app网站搭建做英文网站的心得
  • 东莞企业网站制作推广运营多样化的网站建设公司
  • 深圳做网站那里好提交网址给百度
  • 泰州企业建站系统中企动力科技做什么的
  • 唐山公司网站建设 中企动力唐山宽带动态ip如何做网站访问
  • 个人商城网站怎么做电商网站及企业微信订烟
  • 温州市网站优化广告平面设计教程
  • 南通制作网站的有哪些公司吗sae 部署wordpress