CPU給我們的的啟示
本文轉載自微信公眾號「 小姐姐味道」,轉載本文請聯系 小姐姐味道公眾號。
作為一只鳥,可以邊吃東西邊拉屎么?這對一個養過鸚鵡的人來說,答案是肯定的。
從鳥嘴到鳥大腸,整個過程是串行的。雖然有些曲折,但方向是單一的,出口是一定的。只要鳥能夠克服一些心理上的不悅,它就能辦得到。
這種情況在一些原始的物種身上顯得有些尷尬。比如水螅,它屬于腔腸動物,無性生殖。這幾個單詞都挺唬人,但它的體內只有一個空腔。從哪里吃,就從哪里出。
同是地球上的物種,從長遠看來,我們還是它的近親。
作為高級哺乳動物,我們能夠在呼吸的時候,同時說話,也能夠同時聽到聲音,看到賞心悅目的風景。如果你想的話,也可以辦到鸚鵡做的事。
這些信息被收集之后,一股腦發送給大腦進行處理。有可能是像深度學習一樣,存了一堆權重,但這些信息到底是如何處理的,我們現在還不得而知。
所以程序員們轉而研究計算機,畢竟這個相比起“最后歸途是哲學”的人類大腦,就像是個玩具。
我們都知道,干一堆事干不好,不如集中精力把一件事干好。這并不是說一個人沒能力把所有的事情干好,而是在不同的事務之間切換,是要耗費資源的。
你的大腦好不容易熟悉了一個工作場景,結果突然調度給它另外一項任務,它就要花很長時間切換到新的工作場景中。
有時候代碼寫多了,我就連說話都開始口吃。但一直不停的說說說,就又恢復了。
所以,所有的人都恨零零散散的工作事務。尤其是恨哪些不斷給你小事情,但又毫不相關的任務的領導。
到頭來,感覺做了很多,但一點成果都沒有,感覺人都廢了。
不要怕,我們看看CPU是怎么處理的。
CPU處理任務時不是一直只處理一個,而是通過給每個線程分配CPU時間片,時間片用完了就切換下一個線程。
時間片非常短,一般只有幾十毫秒,CPU通過不停地切換線程執行,但我們幾乎感覺不到任務的停滯。因為對人類來說,高質量的游戲,每秒只需要60幀,就算是流暢了。
這個時間還是相當可觀的,特別是在進程上下文切換次數較多的情況下,很容易導致CPU將大量時間消耗在寄存器,內核棧以及虛擬內存等資源的保存和恢復上,進而大大縮短了真正運行進程的時間。
對于Linux來說。程序在執行過程中通常有用戶態和內核態兩種狀態,CPU對處于內核態根據上下文環境進一步細分,因此有了下面三種狀態:
- 內核態,運行于進程上下文,內核代表進程運行于內核空間。
- 內核態,運行于中斷上下文,內核代表硬件運行于內核空間。
- 用戶態,運行于用戶空間
我們看一下Linux的top命令,是怎么顯示內核態和用戶態的。
如上圖,us就是user的意思;sy就是system的意思。分別代表了用戶態和內核態。
如果sy占用的太高,就有可能是上下文切換和中斷太頻繁了。
那什么是上下文?
所謂的上下文,說白了就是一個環境。比如你去食堂帶著飯盒,去廁所帶著廁紙。要是搞亂了,去廁所帶著飯盒,感覺上就不正常。操作系統為每一個進程,分配了這么一個上下文,用來存放:代碼、數據、用戶堆棧、共享存儲區、寄存器、進程控制塊等。
先不要管里面的細節了,反正內容很多,切換肯定是要有陳本的。比如,廁紙放在家里臥室柜子的第三層小隔間。
vmstat命令顯示的這幾列,就是這么個意思。cs不是csgo的縮寫,它的全拼是context switch。
在每個進程里,也可以看到累加的值。
- [root@localhost ~]# cat /proc/2788/status
- ...
- voluntary_ctxt_switches: 93950
- nonvoluntary_ctxt_switches: 171204
cs如果太高,那就是線程或者進程開的太多了。
上下文切換又分為2種。
讓步式上下文切換和搶占式上下文切換。
下面先說下讓步式上下文切換。我們拿Java中的cas操作來看就可以了。
cas除了 compare and switch原始指令支持以外,還需要一個循環來保證。
- public final long getAndAddLong(Object var1, long var2, long var4) {
- long var6;
- do {
- var6 = this.getLongVolatile(var1, var2);
- } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
- return var6;
- }
代碼放在循環里,在并發量比較高的情況下,如果許多線程反復嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。所以,讓步式上下文切換,是指執行線程主動釋放CPU,與鎖競爭嚴重程度成正比,可通過減少鎖競爭來避免。
而搶占式上下文切換,是指線程因分配的時間片用盡而被迫放棄CPU,或者被其他優先級更高的線程所搶占。一般由于線程數大于CPU可用核心數引起,可通過調整線程數,適當減少線程數來避免。
那為啥Java的線程就能夠比多進程速度快一些呢?因為Java的線程本質上也是一種輕量級進程,但它的虛擬內存等信息是共享的,只需要切換線程的私有數據,寄存器等不共享的數據。即使這樣,也會耗費不少時間。
使用perf命令同樣能夠觀測到這個上下文切換到過程和數量。比如:
- # 跟蹤所有上下文切換,直到Ctrl-C:
- perf record -e context-switches -c 1 -a
- # 包括使用的原始設置(請參閱:man perf_event_open):
- perf record -vv -e context-switches -a
- # 使用堆棧跟蹤的示例上下文切換,直到Ctrl-C:
- perf record -e context-switches -ag
使用perf report即可查看相關結果。
對于計算機來說,效率最高的依然是專心做一件事。一定程度上,你也算是計算機的老板。如果你一直讓它干一些雜活,把它當牛使,那你的計算機效率不一定會高。
有些人很聰明,他一定知道這種來回切換的方式對你的工作效率影響巨大。排除他愚蠢的屬性,就只剩下壞:給你一堆爛七八糟的事,搞得你身心疲憊,最后又和你講結果導向的---一定是你的領導故意為之。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高并發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。