当前位置: 首页 > 科技观察

深入Linux多线程中的信号量Semaphore

时间:2023-03-21 18:58:38 科技观察

了解Semaphore,先从翻译好的Semaphore说起,对多线程有了解的人都听说过,一般我们理解为“信号量”。不过,这个词对于我们来说还是比较陌生的。它与另一个词Signal(信号)有什么关系?要真正理解这个概念,就要从它的翻译说起。其实Semaphore最好的翻译应该是“信号计数”。承认这一点之后,你必须清楚:这和Signal不是一回事!剑桥词典翻译不好理解Signal:简单来说就是消息,是用户、系统或进程向目标进程发送的信息,用于通知目标进程状态改变或系统异常,对应一个异步场景(我在之前的文章中有详细介绍)。信号量:首先是一个变量,其次是一个计数器。它是在多线程环境中使用的工具。创建信号量时,需要设置一个初始值,表示多个任务(线程)可以同时访问某个共享资源。如果任务要访问共享资源,前提是信号量大于0,当任务成功获取资源后,信号量的值减1;如果当前信号量值小于0,说明无法获取信号量,任务必须挂起,等待信号量返回正值的时刻;当任务执行完毕,必须释放信号量,相应的操作就是将信号量的值加1。此外,对信号量的操作(加法、减法)是原子的。互斥锁(Mutex)是信号量初值为1时的一种特殊情况,即同一时刻只有一个任务可以访问共享资源区。Semaphore又懂了,我们想象这样一个场景(上图):如果北京的国家大剧院有一场免费的音乐会演出,但是现在是疫情期间,剧院规定必须限制剧院观众总数,但是每个人都可以中途离开,把票给其他人,其他人可以中途进场。因此,最先到达的一批人先从剧院门口的售票箱里取了票,然后才进场欣赏演出。后来到的人都在门口等着,因为剧场已经坐满了。过了一会儿,有人觉得节目太无聊,早早离开了。他走的时候把票还回去了。这样其他人就拿着这个人的票进场了。后来又有人走了,他却忘了把票放回去。没关系,大不了剧院能容纳的总人数少了一个。上面的例子中,演唱会现场是共享资源区,观众是任务(线程),票箱里的票数是信号量。信号量用作并发限制。由于门票总数是固定的,音乐厅不会人满为患。在上面的例子中,我们允许退场的观众拿走门票。为什么?因为剧场工作人员可以随时在票箱里加一些票(线程制作人)。说到这里,是不是觉得有点眼熟?没错,就是线程池,但还是有些不同,大家自己去品味吧。信号量实战实践信号量类型为sem_t,类型及相关操作定义在头文件semaphore.h中。创建一个信号量intsem_init(sem_t*sem,intpshared,unsignedintvalue);将信号量的值加1intsem_post(sem_t*sem);金额的值为-1intsem_wait(sem_t*sem);信号量被销毁intsem_destroy(sem_t*sem);具体参数和返回值的含义这里不再赘述。示例如下:你一共有三种类型的下载任务(类型id分别为1、2、3),每次从键盘读取一种类型的任务进行下载,但是CPU最多可以执行2次下载同时执行任务(创建两个线程)。#include#include#include#defineMAXNUM(2)sem_tsemDownload;pthread_ta_thread,b_thread,c_thread;intg_phreadNum=1;voidfunc1(void*arg){//等待信号量Value>0sem_wait(&semDownload);printf("===============DownloadingtaskType1================\n");sleep(5);printf("===============FinishedtaskType1=================\n");g_phreadNum--;//等待线程结束pthread_join(a_thread,NULL);}voidfunc2(void*arg){sem_wait(&semDownload);printf("================DownloadingtaskType2===============\n");sleep(3);printf("===============FinishedtaskType2=================\n");g_phreadNum--;pthread_join(b_thread,NULL);}voidfunc3(void*arg){sem_wait(&semDownload);printf("===============DownloadingtaskType3==============\n");sleep(1);printf("===============FinishedtaskType3================\n");g_phreadNum--;pthread_join(c_thread,NULL);}intmain(){//初始化信号量sem_init(&semDownload,0,0);inttaskTypeId;while(scanf("%d",&taskTypeId)!=EOF){//输入0,测试程序能否正常退出if(taskTypeId==0&&g_phreadNum<=1){break;}elseif(taskTypeId==0){printf("Cannotquit,currentrunningthreadnumis%d\n",g_phreadNum-1);}printf("yourchooseDownloadingtaskType%d\n",taskTypeId);//线程数超过2则不下载if(g_phreadNum>MAXNUM){printf("!!!You'vereachedthemaxnumberofthreads!!!\n");continue;}//用户选择下载Taskswitch(taskTypeId){case1://创建线程1pthread_create(&a_thread,NULL,func1,NULL);//信号量+1,然后触发func1的任务sem_post(&semDownload);//总线程数+1g_phreadNum++;break;case2:pthread_create(&b_thread,NULL,func2,NULL);sem_post(&semDownload);g_phreadNum++;break;case3:pthread_create(&c_thread,NULL,func3,NULL);sem_post(&semDownload);g_phreadNum++;break;default:printf("!!!errortaskTypeId%d!!!\n",taskTypeId);break;}}//销毁信号量sem_destroy(&semDownload);return0;}在上面的例子中,采用了pthread_join()方法,即将子线程合并到主线程中,主线程阻塞等待子线程结束,然后子线程线程资源被回收还有一种加入线程的方法:pthread_detach(),即主线程和子线程分离。主线程不需要关注子线程什么时候结束。子线程结束后,自动回收资源。程序运行结果如下:需要注意一点:pthread.h不是linux系统的默认库,gcc编译参数需要手动添加选项:-lpthread、-pthread。