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