安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

Linux下各种锁的理解和使用及总结解决epoll惊群问题(

8月20日 艮山观投稿
  一。锁
  锁出现的原因
  临界资源是什么:多线程执行流所共享的资源
  锁的作用是什么,可以做原子操作,在多线程中针对临界资源的互斥访问。。。保证一个时刻只有一个线程可以持有锁对于临界资源做修改操作。。。
  任何一个线程如果需要修改,向临界资源做写入操作都必须持有锁,没有持有锁就不能对于临界资源做写入操作。
  锁:保证同一时刻只能有一个线程对于临界资源做写入操作(锁地功能)
  再一个直观地代码引出问题,再从指令集的角度去看问题includestdio。hincludestdlib。hincludeunistd。hincludesystypes。hincludepthread。hvoidRoutine(voidarg){intpcount(int)for(inti0;i20000000;i){(pcount);}return(void)0;}intmain(){intcount0;pthreadttid1,tid2,tid3;pthreadcreate(tid1,NULL,Routine,(void)count);pthreadcreate(tid2,NULL,Routine,(void)count);pthreadcreate(tid3,NULL,Routine,(void)count);pthreadjoin(tid1,NULL);pthreadjoin(tid2,NULL);pthreadjoin(tid3,NULL);看一看结果printf(count:d,count);return0;}
  上述一个及其奇怪的结果,这个结果每一次运行都可能是不一样的,Why?按照我们本来的想法是每一个线程20000000结果肯定应该是60000000呀,可以就是达不到这个值
  为何?(深入汇编指令来看)一定将过程放置到汇编指令上去看就可以理解这个过程了。
  a;或者a1;这些操作的汇编操作是几个步骤?
  其实是三个步骤:将数据从内存读取到寄存器中在寄存器中进行对应的运算将数据运算结果从寄存器写回内存
  正常情况下,数据少,操作的线程少,问题倒是不大,想一想要是这样的情况下,操作次数大,对齐操作的线程多,有些线程从中间切入进来了,在运算之后还没写回内存就另外一个线程切入进来同时对于之前的数据进行再写回内存,啥效果,多次操作之后结果确实一次加加操作后的结果。这样的操作(术语叫做函数的重入)我觉得其实就是重入到了汇编指令中间了,还没将上一次运算的结果写回内存就重新对这个内存读取再运算写入,结果肯定和正常的逻辑后的结果不一样呀
  来一幅图片解释一下
  咋办?其实问题很清楚,我们只需要处理的是多条汇编指令不能让它中间被插入其他的线程运算。(要想自己在执行汇编指令的时候别人不插入进来)将多条汇编指令绑定成为一条指令不就OK了嘛。
  也就是原子操作!!!
  不会原子操作?操作系统给咱提供了线程的绑定方式工具呀:mutex互斥锁(互斥量),自旋锁(spinlock),读写锁(readerswriterlock)他们也称作悲观锁。作用都是一个样,将多个汇编指令锁成为一条原子操作(此处的汇编指令也相当于如下的临界资源)
  悲观锁:锁如其名,每次都悲观地认为其他线程也会来修改数据,进行写入操作,所以会在取数据前先加锁保护,当其他线程想要访问数据时,被阻塞挂起
  乐观锁:每次取数据的时候,总是乐观地认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。
  互斥锁
  最为常见使用地锁就是互斥锁,也称互斥量。mutex
  特征,当其他线程持有互斥锁对临界资源做写入操作地时候,当前线程只能挂起等待,让出CPU,存在线程间切换工作
  解释一下存在线程间切换工作:当线程试图去获取锁对临界资源做写入操作时候,如果锁被别的线程正在持有,该线程会保存上下文直接挂起,让出CPU,等到锁被释放出来再进行线程间切换,从新持有CPU执行写入操作
  互斥锁需要进行线程间切换,相比自旋锁而言性能会差上许多,因为自旋锁不会让出CPU,也就不需要进行线程间切换的步骤,具体原理下一点详述
  includestdio。hincludestdlib。hincludeunistd。hincludesystypes。hincludepthread。voidRoutine(voidarg){intpcount(int)for(inti0;i20000000;i){pthreadmutexlock(mtx);(pcount);pthreadmutexunlock(mtx);}return(void)0;}intmain(){pthreadmutexinit(mtx,NULL);intcount0;pthreadttid1,tid2,tid3;pthreadcreate(tid1,NULL,Routine,(void)count);pthreadcreate(tid2,NULL,Routine,(void)count);pthreadcreate(tid3,NULL,Routine,(void)count);pthreadjoin(tid1,NULL);pthreadjoin(tid2,NULL);pthreadjoin(tid3,NULL);看一看结果printf(count:d,count);pthreadmutexdestroy(mtx);销毁锁return0;}
  加互斥量(互斥锁)确实可以达到要求,但是会发现运行时间非常的长,因为线程间不断地切换也需要时间,线程间切换的代价比较大。
  相关视频推荐
  你绕不开的组件锁,4个方面手撕锁的多种实现
  惊群原理、锁的设计方案及绕不开的死锁问题
  学习地址:CCLinux服务器开发后台架构师【零声教育】学习视频教程腾讯课堂
  需要CCLinux服务器架构师学习资料加qun812855908获取(资料包括CC,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCPIP,协程,DPDK,ffmpeg等),免费分享
  自旋锁
  spinlock。自旋锁。
  对比互斥量(互斥锁)而言,获取自旋锁不需要进行线程间切换,如果自旋锁正在被别的线程占用,该线程也不会放弃CPU进行挂起休眠,而是恰如其名的在哪里不断地循环地查看自旋锁保持者(持有者)是否将自旋锁资源释放出来。。。(自旋地原来就是如此)
  口语解释自旋:持有自旋锁的线程不释放自旋锁,那也没有关系呀,我就在这里不断地一遍又一遍地查询自旋锁是否释放出来,一旦释放出来我立马就可以直接使用(因为我并没有挂起等待,不需要像互斥锁还需要进行线程间切换,重新获取CPU,保存恢复上下文等等操作)
  哪正是因为上述这些特点,线程尝试获取自旋锁,获取不到不会采取休眠挂起地方式,而是原地自旋(一遍又一遍查询自旋锁是否可以获取)效率是远高于互斥锁了。那我们是不是所有情况都使用自旋锁就行了呢,互斥锁就可以放弃使用了吗????
  解释自旋锁地弊端:如果每一个线程都仅仅只是需要短时间获取这个锁,那我自旋占据CPU等待是没啥问题地。要是线程需要长时间地使用占据(锁)会造成过多地无端占据CPU资源,俗称站着茅坑不拉屎。。。但是要是仅仅是短时间地自旋,平衡CPU利用率程序运行效率(自旋锁确实是在有些时候更加合适)
  自旋锁需要场景:内核可抢占或者SMP(多处理器)情况下才真正需求(避免死锁陷入死循环,疯狂地自旋,比如递归获取自旋锁。你获取了还要获取,但是又没法释放)
  自旋锁的使用函数其实和互斥锁几乎是一摸一样地,仅仅只是需要将所有的mutex换成spin即可
  仅仅只是在init存在些许不同includestdio。hincludestdlib。hincludeunistd。hincludesystypes。hincludepthread。voidRoutine(voidarg){intpcount(int)for(inti0;i20000000;i){pthreadspinlock(mtx);(pcount);pthreadspinunlock(mtx);}return(void)0;}intmain(){pthreadspininit(mtx,PTHREADPROCESSSHARED);intcount0;pthreadttid1,tid2,tid3;pthreadcreate(tid1,NULL,Routine,(void)count);pthreadcreate(tid2,NULL,Routine,(void)count);pthreadcreate(tid3,NULL,Routine,(void)count);pthreadjoin(tid1,NULL);pthreadjoin(tid2,NULL);pthreadjoin(tid3,NULL);看一看结果printf(count:d,count);pthreadspindestroy(mtx);销毁锁return0;}
  解决上述地问题地方式二,不是使用stdc99而是直接将inti放置到for循环外面读写锁读者写者模式:主要是处理读多写少地情况,和本文后序关联不大,需要的可自行查阅了解二。epoll惊群问题地理解
  何为惊群,池塘一堆,我瞄准一条插过去,但是好似所有的都像是觉着自己正在被插一样的四处逃窜。这个就是惊群的生活一点的理解
  惊群现象其实一点也不少,比如说acceptpthreadcondbroadcast还有多个线程共享epoll监视一个listenfd然后此刻listenfd说来SYN了,放在了SYN队列中,然后完成了三次握手放在了accept队列中了,现在问题是这个connect我应该交付给哪一个线程处理呢。
  多个epoll监视准备工作的线程就是这群(),然后connet就是鱼叉,这一叉下去肯定是所有的epoll线程都会被惊醒(多线程共享listenfd引发的epoll惊群)
  同样如果将上述的多个线程换成多个进程共享监视同一个listenfd就是(多进程的epoll惊群现象)
  咱再画一个草图再来理解一下这个惊群:
  如果是多进程道理是一样滴,仅仅只是将所有的线程换成进程就OK了三。epoll惊群问题地解决
  终是来到了今天的正题了:epoll惊群问题地解决上面了。。。
  首先先说说accept的惊群问题,没想到吧accept平时大家写它的多线程地时候,多个线程同时accept同一个listensock地时候也是会存在惊群问题地,但是accept地惊群问题已经被Linux内核处理了:当有新的连接进入到accept队列的时候,内核唤醒且仅唤醒一个进程来处理
  但是对于epoll的惊群问题,内核却没有直接进行处理。哪既然内核没有直接帮我们处理,我们应该如何针对这种现象做出一定的措施呢?
  惊群效应带来的弊端:惊群现象会造成epoll的伪唤醒,本来epoll是阻塞挂起等待着地,这个时候因为挂起等待是不会占用CPU地但是一旦唤醒就会占用CPU去处理发生地IO事件,但是其实是一个伪唤醒,这个就是对于线程或者进程的无效调度。然而进程或者线程地调取是需要花费代价地,需要上下文切换。需要进行进程(线程)间的不断切换。。。本来多核CPU是用来支持高并发地,但是现在却被用来无效地唤醒,对于多核CPU简直就是一种浪费(浪费系统资源)还会影响系统的性能。
  解决方式(一般是两种)
  Nginx的解决方式:
  加锁:惊群问题发生的前提是多个进程(线程)监听同一个套接字(listensock)上的事件,所以我们只让一个进程(线程)去处理监听套接字就可以了。是否开启accept锁,开启则需要抢锁,以防惊群,默认是关闭的。if(ngxuseacceptmutex){if(ngxacceptdisabled0){ngxacceptdisabled的值是经过算法计算出来的,当值大于0时,说明此进程负载过高,不再接收新连接。}else{尝试抢accept锁,发生错误直接返回if(ngxtrylockacceptmutex(cycle)NGXERROR){}if(ngxacceptmutexheld){抢到锁,设置事件处理标识,后续事件先暂存队列中。flagsNGXPOSTEVENTS;}else{未抢到锁,修改阻塞等待时间,使得下一次抢锁不会等待太久if(timerNGXTIMERINFINITEtimerngxacceptmutexdelay){}}}}
  方式2:使用设置SOREUSEPORT:使得端口号可以复用,如此多个进程或者线程便可以绑定同一个端口号了这样相当于是每一个进程或线程都监视一个listensock
  画两张图来理解一下:
  四、代码演示:includestdio。hincludesysepoll。hincludestdlib。hincludeunistd。hincludesyssocket。hincludestring。hincludeincludepthread。hincludesystypes。hincludefcntl。htypedefstructsockaddrSA;defineCLIENTSIZE1000defineBUFFSIZE256defineSERVEPORT8080defineERREXIT(m)do{perror(m);close(EXITFAILURE);}while(0)intCreateSocket(){intlistensocksocket(AFINET,SOCKSTREAM,0);intreuseport1;if(1setsockopt(listensock,SOLSOCKET,SOREUSEPORT,reuseport,sizeof(reuseport))){ERREXIT(setsocketopt);}structsockaddrinserveA确定服务端协议地址簇memset(serveAdd,0,sizeof(serveAdd));清空serveAdd。sinfamilyAFINET;serveAdd。sinaddr。saddrhtonl(INADDRANY);其实就是0。0。0。0通配地址serveAdd。sinporthtons(SERVEPORT);if(1bind(listensock,(SA)serveAdd,sizeof(serveAdd))){ERREXIT(bind);}if(1listen(listensock,5)){ERREXIT(listen);}}voidsetnoblock(intfd){oldflagfcntl(fd,FGETFL);获取flagif(1fcntl(fd,FSETFL,oldflagONONBLOCK)){ERREXIT(fcnl);}}像epfd中增加监视事件,将监视事件挂在到红黑树上voidaddfd(intepfd,intfd){ev。data。ev。eventsEPOLLINEPOLLERREPOLLET;if(1epollctl(epfd,EPOLLCTLADD,fd,ev)){ERREXIT(epollctl);}setnoblock(fd);设置非阻塞IO,因为是ET}voiddelfd(intepfd,intfd){if(1epollctl(epfd,EPOLLCTLDEL,fd,ev)){ERREXIT(epollctl);}}使用多线程去演示voidRoutine(voidarg){structepolleventevs(structepollevent)calloc(CLIENTSIZE,sizeof(structepollevent));charbuff〔BUFFSIZE〕;每一个线程都创建一个新地监视窗口,但是其实监视在一个port上将问题抛给内核处理,intlistensock(int)intepfdepollcreate(CLIENTSIZE);addfd(epfd,listensock);intcount1;记录监视IO事件地数目while(1){循环监视intnreadyepollwait(epfd,evs,count,1);printf(tid:d线程被唤醒处理IO事件,pthreadself());sleep(2000);for(i0;i){if(evs〔i〕。eventsEPOLLERR){处理错误断开连接等等操作}elseif((evs〔i〕。eventsEPOLLIN)evs〔i〕。data。fdlistensock){socklentclientLstructsockaddrinclientA处理accept操作intconnectsockaccept(listensock,(SA)clientAdd,clientLen);if(connectsock1){ERREXIT(accept);}printf(acceptsucessandfdisd,connectsock);增加监视事件addfd(epfd,connectsock);}elseif(evs〔i〕。eventsEPOLLIN){readdecodecomputeencode修改成监视写事件}elseif(evs〔i〕。eventsEPOLLOUT){write改成读事件}}}free(evs);释放资源}intmain(){此处显示多个线程共享一个listensock看看效果intlistensockCreateSocket();for(i0;i10;i){简单地开十个线程intlistensockCreateSocket();pthreadcreate(tid,NULL,Routine,(void)listensock);pthreaddetach(tid);分离线程}while(1);主线程等待子线程结束return0;}
  上述还没有进行一个每一个进程都对应一个listensock而是多线程共享一个listensock运行结果如下
  所有的线程同时被唤醒了,但是实际上会处理连接的仅仅只是一个线程,intmain(){intlistensockCreateSocket();for(i0;i10;i){简单地开十个线程intlistensockCreateSocket();pthreadcreate(tid,NULL,Routine,(void)listensock);pthreaddetach(tid);分离线程}while(1);主线程等待子线程结束return0;}
  咱仅仅只是将主线程做如上这样一个简单的修改,每一个线程对应一个每一个线程一个独有的监视窗口,将问题抛给内核去处理,让内核去负载均衡:结果如下
  仅仅唤醒一个线程来进行处理连接,解决了惊群问题五。总结本章:
  本文通过介绍两种锁入手,以及为什么需要锁,锁本质就是为了保护,持有锁你就有权力有能力操作写入一定的临界保护资源,没有锁你就不行需要等待,本质其实是将多条汇编指令绑定成原子操作
  然后介绍了惊群现象,通过一个巧妙地例子,扔一颗石子,只是瞄准一条鱼扔过去了,但是整池鱼都被惊醒了,
  对应我们地实际问题就是,多个线程或者进程共同监视同一个listensock然后IO连接事件到来地时候本来仅仅只是需要一个线程醒过来处理即可,但是却会使得所有地线程(进程)全部醒过来,造成不必要地进程线程间切换,多核CPU被浪费喔,系统资源被浪费
  处理方式一。Nginx源码加互斥锁处理二。设置SOREUSEPORT,使得多个进程线程可以同时连接同一个port,为每一个进程线程搞一个listensock。。。将问题抛给内核去处理,让他去负载均衡地仅仅将IO连接事件分配给一个进程或线程
投诉 评论 转载

卑微的爱由奢入俭(个人生活手记)用了三年的手机,外观看起来跟新机无异,然而网络只是4G制式,前些天在客户公司需要开热点给笔记本电脑使用,发现网速简直连2G时代都赶不上,只能蹭同事的热点,真的特别尴尬,作为顾问……Win10无法修改系统时间怎么办?用户使用Win10操作系统(无论是win10专业版还是家庭版)有时候会发现系统右下角的时间和手机、电视的时间不同了,查询之后发现电脑的时间不正确了。完了之后去修改电脑时间,手动……续中央空调新房开始拆改墙体了,所以面临着中央空调要抓紧时间确定下来了,下面我就为大家介绍一下我近期学习、了解的一些中央空调的知识。首先说一下海信中央空调:海信中央空调提出净化空气概……Linux下各种锁的理解和使用及总结解决epoll惊群问题(一。锁锁出现的原因临界资源是什么:多线程执行流所共享的资源锁的作用是什么,可以做原子操作,在多线程中针对临界资源的互斥访问。。。保证一个时刻只有一个线程可以持……华为打印机有自己的核心技术吗,是如何绕过专利壁垒的?华为当然有自己的核心技术,喷墨打印机确实有爱普生和佳能惠普两种不同的喷头技术和专利,几乎属于垄断的存在。但是在激光打印专利这一块却不同。还有华为的激打和iphone一样是叫别人……国家出手!未成年人玩网游遭最强监管!每周限定3天,每天1小时图源:图虫创意被称为史上最严的防止未成年人沉迷网游新规出台,网络游戏概念股再遭核弹级打击!刚刚,为了更好的保护祖国的花朵,切实防止未成年人沉迷网络游戏,国家新闻出版……美国防部将百亿云计算合同授予时间推迟至12月微软(MSFT。智通财经APP获悉,美国国防部周二表示,将把云计算合同(JWCC)授予时间推迟至12月(原定于下月),这可能会对微软(MSFT。US)、亚马逊(AMZN。US)、谷歌(GOOG……微信好友朋友圈出现一条线怎么办?教你不动声色检查好友状态随着互联网的不断发展,智能手机得到了广泛应用,它的出现方便了人与人之间的社交。只要拥有对方的手机号码,就能随时随地进行联系。但是,与其说智能手机方便了人们之间的联系,不如说是社……不小心删除了微信聊天记录,怎么办呢,有重要信息呢?有两种方法可以找回不小心清理了的微信缓存,关键时候真的能救你。1。使用recover恢复安卓手机操作步骤首先打开你的微信找到你想恢复数据的好友在聊天界面输入reco……苹果宣传将于下周举行发布会华为即将发布14英寸大屏手机大事件【Date:2021。09。8】LIFERECORD苹果宣布将于9月15日举办线上发布会昨日晚上,苹果宣布,将于9月15日凌晨1点举办线上发布会。……新能源车行业带你见识不一样的自动化测试提起自动化测试,大家脑中立即能浮现的大多数是Python开发语言、JAVA开发语言、Appium测试框架、Selenium测试框架等,大部分WEB公司只要做自动化测试基本上都离……怎么赚到第一个1000万?怎么赚到第一个1000万?现在到处自媒体平台在宣传1000万有多好挣,多么容易,导致现在月入5000的人都很自卑,不敢说话。1000万到底有多少?1个月挣10……
对平台企业设立反垄断指引,这是深度的管理创新什么无线蓝牙耳机便宜又好用?30003999元手机性价比排名小米11Pro上榜苹果手机的隔空投送怎么使用?三星Flip3第一次用三星手机,第一次用折叠屏,第一感觉太酷人脸识别时候,一定要穿上衣服!不要以为人脸识别就只拍人脸下周资本市场大事提醒你遇到过哪些被白嫖的事情?vivoX70Pro硬件配置堆料在线,为何b站UP主却说是天长江存储全新PCle4。0SSD问世,并宣布全新升级计划9月首款新机官宣9月9日,正式发布,无缘于屏下摄像头麒麟9000库存告急,骁龙888版华为P50Pro马上发布,老美登月遭质疑,我们的宇航员被抬着,他们却是自己走下来的?教育部提醒查不到代码的都是虚假大学本科毕业论文应达到怎样的水平?婚姻里,哪怕只是占其中一条,都可以毁掉你的婚姻学校直饮水设备维护注意事项老人夏季喝水怎么喝需注意这点独锔匠心非遗技艺让残瓷重生三餐造句用三餐造句大全有颈纹怎么办(脖子颈纹很深怎么办)白凤九是谁演的白凤九是谁饰演的高二年级组工作计划宝宝任性不听话怎么办宝宝任性不听话怎么引导

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利