线程/协程/异步的编程模型(CPU利用率为核心)
最近看了一個(gè)b站博主的視頻https://www.bilibili.com/video/av64066246/講到了線程/協(xié)程/異步的編程模型,這里做下記錄
1.線程
上篇文章有聊到進(jìn)程和線程的關(guān)系,但是沒(méi)有涉及到更低層的原理,這里剛好可以將其補(bǔ)充上
我們知道進(jìn)程是組織資源的最小單位,而線程是安排CPU執(zhí)行的最小單位。引進(jìn)線程是為了更好地共享資源。我們也知道在多線程編程模型中,由于cpu是分時(shí)復(fù)用的,所以線程上下文會(huì)涉及到很多次在用戶態(tài)和內(nèi)核態(tài)的上下文切換。操作系統(tǒng)為了保護(hù)自己嚴(yán)格控制用戶程序的資源訪問(wèn),需要調(diào)用外部資源的時(shí)候,往往需要讓操作系統(tǒng)去進(jìn)行調(diào)用(也就是我們常說(shuō)的系統(tǒng)調(diào)用),不需要外部資源的程序運(yùn)行狀態(tài)是用戶態(tài),反之需要內(nèi)核幫忙操作資源此時(shí)就是內(nèi)核態(tài)。
但是很多人在學(xué)習(xí)線程的時(shí)候都會(huì)有這樣的疑問(wèn)?
為什么cpu需要分時(shí),去頻繁切換時(shí)間片執(zhí)行線程?
單核CPU,有3個(gè)要執(zhí)行的線程,先執(zhí)行線程1,讓出時(shí)間片,再執(zhí)行線程2,讓出時(shí)間片,再執(zhí)行線程3,直至所有線程執(zhí)行完畢;
左右兩邊的區(qū)別在于:右邊不對(duì)CPU進(jìn)行時(shí)間分片,右邊只執(zhí)行了兩次線程的上下文切換?,兩側(cè)執(zhí)行的總時(shí)間是一樣的。
疑問(wèn):明明右邊的執(zhí)行效率更高(只進(jìn)行了兩次上下文切換)?多線程存在的意義?
意義:I/O:包括DiskIO(耗時(shí))和NetworkIO
在讀取文件的過(guò)程中,CPU并不直接讀取硬盤(pán),而是對(duì)DMA(DMA全稱(chēng)直接內(nèi)存訪問(wèn)控制器)下達(dá)指令,讓DMA完成文件的讀取。
步驟:
(1)CPU向DMA下達(dá)指令,指令中含有磁盤(pán)設(shè)備信息和需要讀取的文件位置;
(2)DMA告知硬盤(pán)進(jìn)行文件的讀取,文件讀取會(huì)將硬盤(pán)中的內(nèi)容加載到內(nèi)存中;
(3)硬盤(pán)讀取完畢后,硬盤(pán)會(huì)給DMA一個(gè)反饋;
(4)DMA最終以中斷的形式通知CPU:文件讀取完成
(5)最后,CPU從內(nèi)存中去讀取變量的值,即拿到了文件的內(nèi)容
CPU在(1)狀態(tài)后,就處于閑置的狀態(tài),CPU可以去執(zhí)行其他的線程
假設(shè)這3個(gè)線程都是需要讀取文件,看最右邊的紫色線,CPU先讓1號(hào)線程執(zhí)行,1號(hào)線程執(zhí)行:讓DMA進(jìn)行讀取,此時(shí)CPU讓出資源交給其他的線程,接著線程2拿到CPU的控制權(quán),他通知DMA去進(jìn)行文件的讀取,接著線程3也是一樣操作。。。。
此時(shí)CPU的等待時(shí)間X可以讓給其他線程。
文件讀取完成后,三個(gè)DMA以中斷的方式形式通知CPU,他們的時(shí)間點(diǎn)分別為y1,y2,y3,接著線程1又拿到CPU資源,又可以接著進(jìn)行操作
意義的總結(jié):X這塊的執(zhí)行權(quán)(就是DMA的執(zhí)行的過(guò)程中),這塊的CPU不是阻塞的,CPU可以交給其他的線程,其次,CPU總線是可以復(fù)用的,DMA可以充分的利用這些總線,通過(guò)這兩點(diǎn)可以提高CPU的利用率
缺點(diǎn):線程是OS底層的api,線程開(kāi)辟浪費(fèi)時(shí)間,運(yùn)行線程也會(huì)造成線程上下文的切換,用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換,浪費(fèi)CPU切換的時(shí)間
2.協(xié)程
- 編程語(yǔ)言級(jí)別的線程,可以像使用線程一樣使用協(xié)程,但是在OS底層,他并不是線程;
- 協(xié)程全程處于用戶態(tài),可以大量開(kāi)辟,更輕量,接近1K,不用考慮用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)化;,開(kāi)辟上千個(gè)線程是極限,go語(yǔ)言開(kāi)辟協(xié)程的數(shù)量可以達(dá)到千萬(wàn)級(jí)別
在傳統(tǒng)的J2EE系統(tǒng)中都是基于每個(gè)請(qǐng)求占用一個(gè)線程去完成完整的業(yè)務(wù)邏輯(包括事務(wù))。所以系統(tǒng)的吞吐能力取決于每個(gè)線程的操作耗時(shí)。如果遇到很耗時(shí)的I/O行為,則整個(gè)系統(tǒng)的吞吐立刻下降,因?yàn)檫@個(gè)時(shí)候線程一直處于阻塞狀態(tài),如果線程很多的時(shí)候,會(huì)存在很多線程處于空閑狀態(tài)(等待該線程執(zhí)行完才能執(zhí)行),造成了資源應(yīng)用不徹底。
最常見(jiàn)的例子就是JDBC(它是同步阻塞的),這也是為什么很多人都說(shuō)數(shù)據(jù)庫(kù)是瓶頸的原因。這里的耗時(shí)其實(shí)是讓CPU一直在等待I/O返回,說(shuō)白了線程根本沒(méi)有利用CPU去做運(yùn)算,而是處于空轉(zhuǎn)狀態(tài)。而另外過(guò)多的線程,也會(huì)帶來(lái)更多的ContextSwitch開(kāi)銷(xiāo)。
對(duì)于上述問(wèn)題,現(xiàn)階段行業(yè)里的比較流行的解決方案之一就是單線程加上異步回調(diào)這個(gè)我們?cè)诘谌c(diǎn)會(huì)談到。其代表派是node.js以及Java里的新秀Vert.x。
而協(xié)程的目的就是當(dāng)出現(xiàn)長(zhǎng)時(shí)間的I/O操作時(shí),通過(guò)讓出目前的協(xié)程調(diào)度,執(zhí)行下一個(gè)任務(wù)的方式,來(lái)消除ContextSwitch上的開(kāi)銷(xiāo)。
協(xié)程的特點(diǎn):
而與IO多路復(fù)用結(jié)合起來(lái),特別是高度服務(wù)化的今天,http請(qǐng)求會(huì)產(chǎn)生很多socket io操作,tcp包是分段的,一個(gè)socket可讀了,然后可能只讀到了半條請(qǐng)求,需要進(jìn)行頻繁的保存和恢復(fù)線程。
所以很適合用協(xié)程來(lái)調(diào)度。具體可以看這篇文章說(shuō)的真的很好:
https://blog.csdn.net/weixin_39717110/article/details/110722214?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242
協(xié)程的原理
當(dāng)出現(xiàn)IO阻塞的時(shí)候,由協(xié)程的調(diào)度器進(jìn)行調(diào)度,通過(guò)將數(shù)據(jù)流立刻yield掉(主動(dòng)讓出),并且記錄當(dāng)前棧上的數(shù)據(jù),阻塞完后立刻再通過(guò)線程恢復(fù)棧,并把阻塞的結(jié)果放到這個(gè)線程上去跑,這樣看上去好像跟寫(xiě)同步代碼沒(méi)有任何差別,這整個(gè)流程可以稱(chēng)為coroutine,而跑在由coroutine負(fù)責(zé)調(diào)度的線程稱(chēng)為Fiber。比如Golang里的 go關(guān)鍵字其實(shí)就是負(fù)責(zé)開(kāi)啟一個(gè)Fiber,讓func邏輯跑在上面。
由于協(xié)程的暫停完全由程序控制,發(fā)生在用戶態(tài)上;而線程的阻塞狀態(tài)是由操作系統(tǒng)內(nèi)核來(lái)進(jìn)行切換,發(fā)生在內(nèi)核態(tài)上。
因此,協(xié)程的開(kāi)銷(xiāo)遠(yuǎn)遠(yuǎn)小于線程的開(kāi)銷(xiāo),也就沒(méi)有了ContextSwitch上的開(kāi)銷(xiāo)。
比較線程協(xié)程:
1、占用資源初始單位為1MB,固定不可變初始一般為 2KB,可隨需要而增大調(diào)度所屬由 OS 的內(nèi)核完成由用戶完成。
2、切換開(kāi)銷(xiāo)涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài))、16個(gè)寄存器、PC、SP...等寄存器的刷新等只有三個(gè)寄存器的值修改 - PC / SP / DX.性能問(wèn)題資源占用太高,頻繁創(chuàng)建銷(xiāo)毀會(huì)帶來(lái)嚴(yán)重的性能問(wèn)題資源占用小,不會(huì)帶來(lái)嚴(yán)重的性能問(wèn)題。
3、數(shù)據(jù)同步需要用鎖等機(jī)制確保數(shù)據(jù)的一直性和可見(jiàn)性不需要多線程的鎖機(jī)制,因?yàn)橹挥幸粋€(gè)線程,也不存在同時(shí)寫(xiě)變量沖突,在協(xié)程中控制共享資源不加鎖,只需要判斷狀態(tài)就好了,所以執(zhí)行效率比多線程高很多。比如在java中,每個(gè)線程都會(huì)從內(nèi)存中拷貝自己的棧,然后在線程完成后回寫(xiě)到內(nèi)存中,所以高并發(fā)的時(shí)候我們經(jīng)常為線程加上鎖,這個(gè)鎖很多時(shí)候浪費(fèi)了,而協(xié)程由于在一個(gè)線程中,所以根本不會(huì)有這個(gè)問(wèn)題。
3.異步
在1中的(4)DMA最終以中斷的形式通知CPU:文件讀取完成就是異步操作,Node.js是單線程的,卻可以應(yīng)對(duì)高并發(fā),就是因?yàn)樗钱惒降摹T贜ode.js中有大量的回調(diào)函數(shù)的產(chǎn)生,大量的異步操作eg:在單線程的執(zhí)行過(guò)程中,出現(xiàn)了IO讀寫(xiě),就會(huì)交給DMA去進(jìn)行文件的讀寫(xiě),單線程繼續(xù)工作,最終在觸發(fā)的時(shí)候,會(huì)觸發(fā)一個(gè)回調(diào)函數(shù),獲取到一個(gè)文件的數(shù)據(jù)
總結(jié):所有的這些方法模型都是基于一個(gè)目的,cpu-》內(nèi)存-〉磁盤(pán)速度差距太大,cpu會(huì)空出很多空閑時(shí)間,所以需要這樣來(lái)提高cpu的利用率。但是同時(shí)也提高了我們寫(xiě)程序的復(fù)雜度。
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的线程/协程/异步的编程模型(CPU利用率为核心)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 关于CPU指标的解释
- 下一篇: stream流【java8 二】