CPU和內存基礎,執行任務時性能參數的理解,以及對應的Python和Java運行時的進程和線程情況

CPU(Central Processing Unit)

CPU是一塊超大規模的集成電路,是一臺計算機的運算核心(Core)和控制核心( Control Unit)。它的功能主要是解釋計算機指令、處理計算機軟件中的數據。主要組成分爲三個部分,由CPU內部總線連接起來:

  • 算術邏輯運算單元(ALU,Arithmetic Logic Unit):負責執行所有的數學和邏輯工作
  • 控制單元(CU,Control Unit):根據用戶預先編好的程序,依次從存儲器中取出各條指令,放在指令寄存器IR中,通過指令譯碼(分析)確定應該進行什麼操作,然後通過操作控制器OC,按確定的時序,向相應的部件發出微操作控制信號。
  • 存儲單元:包括CPU片內緩存和寄存器組,是CPU中暫時存放數據的地方。採用寄存器,可以減少CPU訪問內存的次數,從而提高了CPU的工作速度。
    寄存器組可分爲專用寄存器和通用寄存器。專用寄存器的作用是固定的,分別寄存相應的數據。而通用寄存器用途廣泛並可由程序員規定其用途,通用寄存器的數目因微處理器而異。

同一個時刻單個CPU核上只能運行一個任務,即一個核線程(kernel thread)

單個CPU多個內核

多核處理器是指在一枚處理器中集成兩個或多個完整的計算引擎(內核),核心是多個,但是其他硬件還都是多個核心所共同擁有,如L3緩存
一個CPU的每個核心擁有獨立的運算處理單元、控制器、寄存器、L1、L2緩存
多個核心共享最後一層CPU緩存L3,是一個進程的多個線程可以共享信息的基礎

單個核心多個線程

Intel開發出了超線程技術(HyperthreadingTechnology),可以在一個物理內核上模擬出兩個邏輯內核,即平時常見的4核8線程。其本質原理是讓兩個核線程同時使用CPU中的不同部分(處理核心,寄存器,緩存)
因爲邏輯內核是模擬出來的,因此就算是雙核四線程還是隻有2個一級緩存L1,2個二級緩存L2,一個三級緩存L3

多CPU

真正意義上的多CPU不光是處理器核心是兩個,其他例如緩存等硬件配置也都是雙份的。一個CPU對應一個物理插槽,多處理器間通過QPI總線相連。

串行,併發,並行

串行:多個線程,執行時一個執行完再執行另一個。
併發(concurrency):多個線程以短暫的時間片爲單位交替執行。
並行(parallelism):每個線程分配給獨立的核心,同時運行。

內存

內存是由一系列的存儲單元組成的,每個存儲單元存儲固定大小的數據,且有一個唯一地址。

當需要讀內存時,將地址信號放到地址總線上傳給內存,內存解析信號並定位到存儲單元,然後把該存儲單元上的數據放到數據總線上,回傳。寫內存時,系統將要寫入的數據和單元地址分別放到數據總線和地址總線上,內存讀取兩個總線的內容,做相應的寫操作。

內存存取效率,跟次數有關。

構成內存的SDRAM(同步動態隨機存儲器)是一種斷電即掉的非永久隨機存儲器。只含有一個晶體管和一個電容器,集成度非常高可用輕易的做到大容量,但因爲靠電容器來存儲信息所以需要不間斷刷新電容器的電荷,而充放電之間的時間差導致DRAM的數據讀寫速度較SRAM慢的多。

OS內核啓動以後, 內核將物理內存管理起來。內核提供虛擬內存管理機制給每個進程(應用程序App)內存服務。

進程(應用App)需要內存時,OS給其分配一個獨立,相互不影響的虛擬內存空間。然後OS再從自己管理的物理內存裏面分配出來物理內存頁,然後通過頁表或者段表,將分配的虛擬內存與物理內存映射起來,這樣,讀寫虛擬內存地址最終通過映射來使用物理內存地址,

內存分配策略:可變大小分配策略,固定尺寸分配策略
內存分配的開銷:維護分配和釋放內存空間的大小(可變大小分配策略下)的開銷,對齊開銷

內存分配由誰負責

分層次,由內到外:

  1. 操作系統內核提供最基本服務
  2. 編譯器的缺省運行庫提供自己的分配服務,可能只是對操作系統的算法進行了封裝,也可能有重載
  3. 標準運行環境(框架等)提供的內存分配服務
  4. 用戶自定義的運行環境(框架等)和分配器提供的服務

每個進程會把虛擬內存空間分成4個段(代碼段, 數據段,堆,棧)。

  • 代碼段:用來存放進程(應用App)的代碼指令。
  • 數據段:用來存放全局變量的內存。
  • 堆:調用os的malloc/free 來動態分配的內存。
  • 棧:用來存放局部變量,函數參數,函數調用與跳轉。

1頁表與物理內存映射關係

CPU緩存

CPU的運算處理速度與內存讀寫速度的差異非常巨大,爲了解決這種差異,CPU緩存應運而生,它是介於CPU處理器和內存之間的臨時數據交換的緩衝區。

物理原理

由SRAM(靜態隨機存儲器)構成,相比起DRAM更復雜,佔據空間大,成本高,集成度低,因爲不需要刷新電路所以讀寫速度快。

邏輯原理

時間局部性原理:被引用過的內存位置很可能在不遠的將來還會被多次引用。
空間局部性原理:如果一個內存位置被引用了,那麼程序很可能會在不遠的將來引用該內存位置附近的內存位置。

多個核心共享CPU緩存是多個線程可以共享信息的基礎

緩存分爲數據緩存(Data Cache,D-Cache)和一級指令緩存(Instruction Cache,I-Cache),分別用於存放數據和執行數據的指令解碼,兩者可以同時被CPU訪問,減少了CPU多核心,多線程爭用緩存造成的衝突。

從L0, L1, L2到磁盤,每一層存儲的數據都是下一層的數據的子集

因爲時間侷限性原理,CPU緩存在不命中的時候,向下層緩存請求的時候,返回的數據是以一個緩存行爲單位的。直到到達L1。L1將數據行放置到自己的緩存行之後,從被存儲的緩存行中抽取出CPU真正需要的字w,然後將它返回給CPU。

CPU緩存讀寫

從CPU緩存讀取時從上往下依次查找的,下層查找到包含字w的緩存行之後,再由下層將該緩存行返回給上一層高速緩存,上一層高速緩存將這個緩存行放在它自己的一個高速緩存行中之後,繼續返回給上一層,直到到達L1。

寫一個已經緩存了的字w(寫命中):更新本級緩存的w副本之後,依次更新它的下一級緩存。有兩個思路:

  1. 直寫:即立即將包含w的高速緩存行寫回到第一層的緩存層, 這樣做簡單,但是如果大家都不停的寫勢必會產生很大的總線流量,不利於其他數據的處理。
  2. 寫回:儘可能的推遲更新,只有當需要使用這個更新過的緩存行時才把它寫回到緊接着的第一層的緩存中,這樣總量流量減少了,但是增加了複雜性,高速緩存行必須額外的維護一個“修改位”,表明這個高速緩存行是否被修改過。

寫一個不在緩存中的字w時(寫不命中):也有兩種思路

  1. 寫分配,就是把不命中的緩存先加載過來,然後再更新整個緩存行,後面就是寫命中的處理邏輯了
  2. 非寫分配:直接把這個字寫到下一層。

保持多CPU緩存一致性的方法:

緩存一致性協議MESI

  1. 寫回/懶更新:當一個CPU在修改它的緩存之前,會通過最後一級緩存L3(因爲最後一級緩存是多核心共享的)或者總線(多CPU跨插槽的情況)廣播到其他CPU的緩存,使其它存在該緩存數據的緩存行無效,然後再更改自己的緩存數據,並標記爲M,當其他CPU緩存需要讀取這個被修改過的緩存行時(或者由於衝突不命中需要被置換出去時),會導致立即將這個被修改過的緩存行寫回到內存,然後再從內存加載最新的數據到自己的緩存行。
  2. 使用“直瀉時”,更改馬上寫回內存,其他CPU通過嗅探技術,從總線上得知相關的緩存行數據失效,則立即使自己相應的緩存行無效,從而再下次讀不命中的時候重新到內存加載最新的數據。
  3. 當CPU修改自己的緩存行數據時,主動將相關的更新通過最後一級緩存L3或者總線(如果是多CPU跨插槽的情況)發送給其它存在相關緩存的CPU,使它們同步的更新自己的緩存到一致。

操作系統內核

內核是操作系統的核心。是操作系統執行的第一道程序,被率先加載到內存中開始系統行爲。內核始終保持在主內存中直到系統被關閉。

內核將用戶輸入的命令轉換成計算機硬件能理解的機器語言將,是系統應用軟件和硬件的橋樑。

內核的主要職責是:進程管理、磁盤管理、任務調度、內存管理等。

進程,線程,協程,任務調度器

進程

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,是系統資源分配和獨立運行的最小單位。
代碼是靜態的指令,而進程就是動態執行任務的過程。
每個進程都有自己的獨立內存空間,通信和上下文進程間的切換(棧、寄存器、虛擬內存、文件句柄等)開銷比較大,但相對比較穩定安全。

線程

線程是任務調度和系統執行的最小單位
線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),
一個進程可以擁有多個線程,同屬一個進程的線程共享進程所擁有的全部資源。
線程間通信主要通過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
單核環境下,多線程時間片輪轉調度併發執行。
時間片輪轉調度:CPU將時間分成小片,每個時間片輪流執行所有線程,看起來就好像所有線程同時在進行一樣,這種情況下每個線程需要的運行時間比單個計算任務本身耗時要長。
多核環境下,多線程會被分發給不同的內核並行執行,如果內核數量不夠則分片輪轉調度併發執行。

嚴格講應該是線程能夠獲得CPU資源,進程對CPU資源的獲取也是體現在線程上的。至於CPU內核數,和進程線程沒直接關係。操作系統(OS)可以把某個進程部署在某個CPU核上,當然這要取決於系統設計。

堆內存、代碼區一般屬於一個進程,但是棧卻是屬於一個線程的,且每個線程擁有一個獨立的棧

協程

協程是一種用戶態的輕量級線程,狀態切換及上下文切換等資源調度完全由用戶控制,不受內核調度
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷
協程可以不加鎖的訪問全局變量,所以上下文的切換非常快

計算密集型任務併發時需要思考的問題

  1. 這樣的任務CPU使用率高,爲了防止CPU爆滿其實並不應該開特別多的線程,可以通過隊列限制併發數量
  2. 注意計算密集型任務長時間佔用CPU,導致其他請求回覆過慢的情況。應該將CPU佔用率高的任務提煉成單獨的微服務,再配以適當的計算資源(基本+高峯彈性)

IO密集型任務併發時需要思考的問題

  1. 因爲大多數時間會花費在等待IO阻塞上,可以多開一些線程,充分利用CPU
  2. 注意防止大量線程阻塞,或同時讀取數據到內存導致的內存溢出

爲什麼單機CPU邏輯內核數最多不過20幾,網頁服務器(tomcat,gunicorn等)默認的同時接受請求數卻會達到1000?

默認的網絡請求消耗的計算時間其實不高,線程併發雖然會增加每個請求處理的耗時,但一個線程完成任務消耗20ms還是50ms用戶並感受不到差別,而且相比起網絡IO幾百毫秒的延遲幾乎可以忽略

Python高併發執行計算密集型任務時的情況

GIL(Global Interceptor Lock)是Python爲了防止解釋器資源競爭而設立的機制,一個進程只有一個GIL,當有多個線程時,線程競爭GIL,只有獲取到GIL的線程纔可以運行
由於GIL鎖的影響,Python的一個進程永遠都只有一個線程正在執行,因此Python的多線程無法利用多CPU的計算環境
想要在Python實現計算密集型任務的高併發,只能依靠多進程,網絡應用可以依靠uswgi等網絡服務器自帶的多worker機制,本地應用可以利用multiprocessing庫
因爲是多進程而不是多線程,所以CPU和內存溢出比Java更好控制
協程在IO密集型任務時也可以幫助提高併發,但面對計算密集型任務效果並不好

Java高併發執行計算密集ç任務時的情況

啓動java程序會固定在jvm上啓動一個進程,因此單個java應用的任務併發主要依靠多線程來解決,操作系統會將不同的java線程分配到不同的CPU內核上
單個應用內還可以通過鎖核操作防止計算密集型任務對其他任務的影響

Reference

計算機組成原理:CPU、核心,進程、線程,串行、併發、並行
CPU、內存、進程、線程原理
知乎 - Java 多線程如何實現在多 CPU 上分佈?(用戶【Java技術那些事
】的回答)
淺談Python和Java的多進程與多線程的異同

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章