python GIL鎖與多cpu

多核CPU
linux

 

linux :程序員

cat /proc/cpuinfo

若是你不幸擁有一個多核CPU,你確定在想,多核應該能夠同時執行多個線程。面試

若是寫一個死循環的話,會出現什麼狀況呢?編程

打開Mac OS X的Activity Monitor,或者Windows的Task Manager,均可以監控某個進程的CPU使用率。瀏覽器

咱們能夠監控到一個死循環線程會100%佔用一個CPU。若是有兩個死循環線程,在多核CPU中,能夠監控到會佔用200%的CPU,也就是佔用兩個CPU核心。要想把N核CPU的核心所有跑滿,就必須啓動N個死循環線程。緩存

試試用Python寫個死循環:安全

?
1
2
3
4
5
6
7
8
9
10
import threading, multiprocessing
 
def loop():
   x = 0
   while True :
     x = x ^ 1
 
for i in range (multiprocessing.cpu_count()):
   t = threading.Thread(target = loop)
   t.start()

啓動與CPU核心數量相同的N個線程,在4核CPU上能夠監控到CPU佔用率僅有102%,也就是僅使用了一核。服務器

可是用C、C++或Java來改寫相同的死循環,直接能夠把所有核心跑滿,4核就跑到400%,8核就跑到800%,爲何Python不行呢?網絡

由於Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先得到GIL鎖,而後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把全部線程的執行代碼都給上了鎖,因此,多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。多線程

GIL是Python解釋器設計的歷史遺留問題,一般咱們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

因此,在Python中,可使用多線程,但不要期望能有效利用多核。若是必定要經過多線程利用多核,那隻能經過C擴展來實現,不過這樣就失去了Python簡單易用的特色。

不過,也不用過於擔憂,Python雖然不能利用多線程實現多核任務,但能夠經過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

多線程編程,模型複雜,容易發生衝突,必須用鎖加以隔離,同時,又要當心死鎖的發生。

Python解釋器因爲設計時有GIL全局鎖,致使了多線程沒法利用多核。

 

進程和線程在多核cpu,多cpu中的運行關係

多cpu的運行,對應進程的運行狀態;多核cpu的運行,對應線程的運行狀態。

操做系統會拆分CPU爲一段段時間的運行片,輪流分配給不一樣的程序。對於多cpu,多個進程能夠並行在多個cpu中計算,固然也會存在進程切換;對於單cpu,多個進程在這個單cpu中是併發運行,根據時間片讀取上下文+執行程序+保存上下文。同一個進程同一時間段只能在一個cpu中運行,若是進程數小於cpu數,那麼未使用的cpu將會空閒。

進程有本身的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,創建數據表來維護代碼段、堆棧段和數據段,這種操做很是昂貴。而線程是共享進程中的數據的,使用相同的地址空間,所以CPU切換一個線程的花費遠比進程要小不少,同時建立一個線程的開銷也比進程要小不少。
對於多核cpu,進程中的多線程並行執行,執行過程當中存在線程切換,線程切換開銷較小。對於單核cpu,多線程在單cpu中併發執行,根據時間片切換線程。同一個線程同一時間段只能在一個cpu內核中運行,若是線程數小於cpu內核數,那麼將有多餘的內核空閒。

總結

1 單CPU中進程只能是併發,多CPU計算機中進程能夠並行。

2單CPU單核中線程只能併發,單CPU多核中線程能夠並行。

3 不管是併發仍是並行,使用者來看,看到的是多進程,多線程。

 

進程 vs. 線程

咱們介紹了多進程和多線程,這是實現多任務最經常使用的兩種方式。如今,咱們來討論一下這兩種方式的優缺點。

首先,要實現多任務,一般咱們會設計Master-Worker模式,Master負責分配任務,Worker負責執行任務,所以,多任務環境下,一般是一個Master,多個Worker。

若是用多進程實現Master-Worker,主進程就是Master,其餘進程就是Worker。

若是用多線程實現Master-Worker,主線程就是Master,其餘線程就是Worker。

多進程模式最大的優勢就是穩定性高,由於一個子進程崩潰了,不會影響主進程和其餘子進程。(固然主進程掛了全部進程就全掛了,可是Master進程只負責分配任務,掛掉的機率低)著名的Apache最先就是採用多進程模式。

多進程模式的缺點是建立進程的代價大,在Unix/Linux系統下,用fork調用還行,在Windows下建立進程開銷巨大。另外,操做系統能同時運行的進程數也是有限的,在內存和CPU的限制下,若是有幾千個進程同時運行,操做系統連調度都會成問題。

多線程模式一般比多進程快一點,可是也快不到哪去,並且,多線程模式致命的缺點就是任何一個線程掛掉均可能直接形成整個進程崩潰,由於全部線程共享進程的內存。在Windows上,若是一個線程執行的代碼出了問題,你常常能夠看到這樣的提示:「該程序執行了非法操做,即將關閉」,其實每每是某個線程出了問題,可是操做系統會強制結束整個進程。

在Windows下,多線程的效率比多進程要高,因此微軟的IIS服務器默認採用多線程模式。因爲多線程存在穩定性的問題,IIS的穩定性就不如Apache。爲了緩解這個問題,IIS和Apache如今又有多進程+多線程的混合模式,真是把問題越搞越複雜。

線程切換

不管是多進程仍是多線程,只要數量一多,效率確定上不去,爲何呢?

咱們打個比方,假設你不幸正在準備中考,天天晚上須要作語文、數學、英語、物理、化學這5科的做業,每項做業耗時1小時。

若是你先花1小時作語文做業,作完了,再花1小時作數學做業,這樣,依次所有作完,一共花5小時,這種方式稱爲單任務模型,或者批處理任務模型。

假設你打算切換到多任務模型,能夠先作1分鐘語文,再切換到數學做業,作1分鐘,再切換到英語,以此類推,只要切換速度足夠快,這種方式就和單核CPU執行多任務是同樣的了,以幼兒園小朋友的眼光來看,你就正在同時寫5科做業。

可是,切換做業是有代價的,好比從語文切到數學,要先收拾桌子上的語文書本、鋼筆(這叫保存現場),而後,打開數學課本、找出圓規直尺(這叫準備新環境),才能開始作數學做業。操做系統在切換進程或者線程時也是同樣的,它須要先保存當前執行的現場環境(CPU寄存器狀態、內存頁等),而後,把新任務的執行環境準備好(恢復上次的寄存器狀態,切換內存頁等),才能開始執行。這個切換過程雖然很快,可是也須要耗費時間。若是有幾千個任務同時進行,操做系統可能就主要忙着切換任務,根本沒有多少時間去執行任務了,這種狀況最多見的就是硬盤狂響,點窗口無反應,系統處於假死狀態。

因此,多任務一旦多到一個限度,就會消耗掉系統全部的資源,結果效率急劇降低,全部任務都作很差。

計算密集型 vs. IO密集型

是否採用多任務的第二個考慮是任務的類型。咱們能夠把任務分爲計算密集型和IO密集型。

計算密集型任務的特色是要進行大量的計算,消耗CPU資源,好比計算圓周率、對視頻進行高清解碼等等,全靠CPU的運算能力。這種計算密集型任務雖然也能夠用多任務完成,可是任務越多,花在任務切換的時間就越多,CPU執行任務的效率就越低,因此,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數。

計算密集型任務因爲主要消耗CPU資源,所以,代碼運行效率相當重要。Python這樣的腳本語言運行效率很低,徹底不適合計算密集型任務。對於計算密集型任務,最好用C語言編寫。

第二種任務的類型是IO密集型,涉及到網絡、磁盤IO的任務都是IO密集型任務,這類任務的特色是CPU消耗不多,任務的大部分時間都在等待IO操做完成(由於IO的速度遠遠低於CPU和內存的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部分任務都是IO密集型任務,好比Web應用。

IO密集型任務執行期間,99%的時間都花在IO上,花在CPU上的時間不多,所以,用運行速度極快的c語言替換用Python這樣運行速度極低的腳本語言,徹底沒法提高運行效率。對於IO密集型任務,最合適的語言就是開發效率最高(代碼量最少)的語言,腳本語言是首選,C語言最差。

異步IO

考慮到CPU和IO之間巨大的速度差別,一個任務在執行的過程當中大部分時間都在等待IO操做,單進程單線程模型會致使別的任務沒法並行執行,所以,咱們才須要多進程模型或者多線程模型來支持多任務併發執行。

現代操做系統對IO操做已經作了巨大的改進,最大的特色就是支持異步IO。若是充分利用操做系統提供的異步IO支持,就能夠用單進程單線程模型來執行多任務,這種全新的模型稱爲事件驅動模型,Nginx就是支持異步IO的Web服務器,它在單核CPU上採用單進程模型就能夠高效地支持多任務。在多核CPU上,能夠運行多個進程(數量與CPU核心數相同),充分利用多核CPU。因爲系統總的進程數量十分有限,所以操做系統調度很是高效。用異步IO編程模型來實現多任務是一個主要的趨勢。

對應到Python語言,單進程的異步編程模型稱爲協程,有了協程的支持,就能夠基於事件驅動編寫高效的多任務程序。咱們會在後面討論如何編寫協程。

分佈式進程

在Thread和Process中,應當優選Process,由於Process更穩定,並且,Process能夠分佈到多臺機器上,而Thread最多隻能分佈到同一臺機器的多個CPU上。

Python的multiprocessing模塊不但支持多進程,其中managers子模塊還支持把多進程分佈到多臺機器上。一個服務進程能夠做爲調度者,將任務分佈到其餘多個進程中,依靠網絡通訊。因爲managers模塊封裝很好,沒必要了解網絡通訊的細節,就能夠很容易地編寫分佈式多進程程序。

舉個例子:若是咱們已經有一個經過Queue通訊的多進程程序在同一臺機器上運行,如今,因爲處理任務的進程任務繁重,但願把發送任務的進程和處理任務的進程分佈到兩臺機器上。怎麼用分佈式進程實現?

原有的Queue能夠繼續使用,可是,經過managers模塊把Queue經過網絡暴露出去,就可讓其餘機器的進程訪問Queue了。

咱們先看服務進程,服務進程負責啓動Queue,把Queue註冊到網絡上,而後往Queue裏面寫入任務:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import random, time, queue
from multiprocessing.managers import BaseManager
 
# 發送任務的隊列:
task_queue = queue.Queue()
# 接收結果的隊列:
result_queue = queue.Queue()
 
# 從BaseManager繼承的QueueManager:
class QueueManager(BaseManager):
   pass
 
# 把兩個Queue都註冊到網絡上, callable參數關聯了Queue對象:
QueueManager.register( 'get_task_queue' , callable = lambda : task_queue)
QueueManager.register( 'get_result_queue' , callable = lambda : result_queue)
# 綁定端口5000, 設置驗證碼'abc':
manager = QueueManager(address = (' ', 5000), authkey=b' abc')
# 啓動Queue:
manager.start()
# 得到經過網絡訪問的Queue對象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放幾個任務進去:
for i in range ( 10 ):
   n = random.randint( 0 , 10000 )
   print ( 'Put task %d...' % n)
   task.put(n)
# 從result隊列讀取結果:
print ( 'Try get results...' )
for i in range ( 10 ):
   r = result.get(timeout = 10 )
   print ( 'Result: %s' % r)
# 關閉:
manager.shutdown()
print ( 'master exit.' )

當咱們在一臺機器上寫多進程程序時,建立的Queue能夠直接拿來用,可是,在分佈式多進程環境下,添加任務到Queue不能夠直接對原始的task_queue進行操做,那樣就繞過了QueueManager的封裝,必須經過manager.get_task_queue()得到的Queue接口添加。

而後,在另外一臺機器上啓動任務進程(本機上啓動也能夠):

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time, sys, queue
from multiprocessing.managers import BaseManager
 
# 建立相似的QueueManager:
class QueueManager(BaseManager):
   pass
 
# 因爲這個QueueManager只從網絡上獲取Queue,因此註冊時只提供名字:
QueueManager.register( 'get_task_queue' )
QueueManager.register( 'get_result_queue' )
 
# 鏈接到服務器,也就是運行task_master.py的機器:
server_addr = '127.0.0.1'
print ( 'Connect to server %s...' % server_addr)
# 端口和驗證碼注意保持與task_master.py設置的徹底一致:
m = QueueManager(address = (server_addr, 5000 ), authkey = b 'abc' )
# 從網絡鏈接:
m.connect()
# 獲取Queue的對象:
task = m.get_task_queue()
result = m.get_result_queue()
# 從task隊列取任務,並把結果寫入result隊列:
for i in range ( 10 ):
   try :
     n = task.get(timeout = 1 )
     print ( 'run task %d * %d...' % (n, n))
     r = '%d * %d = %d' % (n, n, n * n)
     time.sleep( 1 )
     result.put(r)
   except Queue.Empty:
     print ( 'task queue is empty.' )
# 處理結束:
print ( 'worker exit.' )

 

https://www.jb51.net/article/120706.htm

進程切換與線程切換區別和代價

虛擬內存解放生產力

對於程序員來講,咱們在編程時其實是不怎麼操心內存問題的,對於使用Java、Python、JavaScript等動態類型語言的程序員來講更是如此,自動內存回收機制的引入使得使用這類語言的程序員幾乎徹底不用關心內存問題;即便對於編譯型語言C/C++來講,程序員須要關心的也僅僅是內存的申請和釋放。

總的來講,做爲程序員(不管使用什麼類型的語言)咱們根本就不關心數據以及程序被放在了物理內存的哪一個位置上(設計實現操做系統的程序員除外),咱們能夠簡單的認爲咱們的程序獨佔內存,好比在32位系統下咱們的進程佔用的內存空間爲4G;而且咱們能夠申請超過物理內存大小的空間,好比在只有256MB的系統上程序員能夠申請1G大小的內存空間,這種假設極大的解放了程序員的生產力。

而這種假設實現的背後功臣就是虛擬內存。

 

什麼是虛擬內存

虛擬內存是操做系統爲每一個進程提供的一種抽象,每一個進程都有屬於本身的、私有的、地址連續的虛擬內存,固然咱們知道最終進程的數據及代碼必然要放到物理內存上,那麼必須有某種機制能記住虛擬地址空間中的某個數據被放到了哪一個物理內存地址上,這就是所謂的地址空間映射,也就是虛擬內存地址與物理內存地址的映射關係,那麼操做系統是如何記住這種映射關係的呢,答案就是頁表,頁表中記錄了虛擬內存地址到物理內存地址的映射關係。有了頁表就能夠將虛擬地址轉換爲物理內存地址了,這種機制就是虛擬內存。

每一個進程都有本身的虛擬地址空間,進程內的全部線程共享進程的虛擬地址空間。

如今咱們就能夠來回答這個面試題了。

 

進程切換與線程切換的區別

進程切換與線程切換的一個最主要區別就在於進程切換涉及到虛擬地址空間的切換而線程切換則不會。由於每一個進程都有本身的虛擬地址空間,而線程是共享所在進程的虛擬地址空間的,所以同一個進程中的線程進行線程切換時不涉及虛擬地址空間的轉換。

舉一個不太恰當的例子,線程切換就比如你從主臥走到次臥,反正主臥和次臥都在同一個房子中(虛擬地址空間),所以你無需換鞋子、換衣服等等。可是進程切換就不同了,進程切換就比如從你家到別人家,這是兩個不一樣的房子(不一樣的虛擬地址空間),出發時要換好衣服、鞋子等等,到別人家後還要再換鞋子等等。

所以咱們能夠形象的認爲線程是處在同一個屋檐下的,這裏的屋檐就是虛擬地址空間,所以線程間切換無需虛擬地址空間的切換;而進程則不一樣,兩個不一樣進程位於不一樣的屋檐下,即進程位於不一樣的虛擬地址空間,所以進程切換涉及到虛擬地址空間的切換,這也是爲何進程切換要比線程切換慢的緣由。

有的同窗可能仍是不太明白,爲何虛擬地址空間切換會比較耗時呢?

 

爲何虛擬地址切換很慢

如今咱們已經知道了進程都有本身的虛擬地址空間,把虛擬地址轉換爲物理地址須要查找頁表,頁表查找是一個很慢的過程,所以一般使用Cache來緩存經常使用的地址映射,這樣能夠加速頁表查找,這個cache就是TLB,Translation Lookaside Buffer,咱們不須要關心這個名字只須要知道TLB本質上就是一個cache,是用來加速頁表查找的。因爲每一個進程都有本身的虛擬地址空間,那麼顯然每一個進程都有本身的頁表,那麼當進程切換後頁表也要進行切換,頁表切換後TLB就失效了,cache失效致使命中率下降,那麼虛擬地址轉換爲物理地址就會變慢,表現出來的就是程序運行會變慢,而線程切換則不會致使TLB失效,由於線程線程無需切換地址空間,所以咱們一般說線程切換要比較進程切換塊,緣由就在這裏。

 

 

單核cpu和多核cpu
都是一個cpu,不一樣的是每一個cpu上的核心數。

多核cpu是多個單核cpu的替代方案,多核cpu減少了體積,同時也減小了功耗。

一個核心只能同時執行一個線程。


進程和線程
理解
進程是操做系統進行資源(包括cpu、內存、磁盤IO等)分配的最小單位。

線程是cpu調度和分配的基本單位。

咱們打開的聊天工具,瀏覽器都是一個進程。

進程可能有多個子任務,好比聊天工具要接受消息,發送消息,這些子任務就是線程。

資源分配給進程,線程共享進程資源。


對比
對比 進程 線程
定義 進程是程序運行的一個實體的運行過程,是系統進行資源分配和調配的一個獨立單位 線程是進程運行和執行的最小調度單位
系統開銷 建立撤銷切換開銷大,資源要從新分配和收回 僅保存少許寄存器的內容,開銷小,在進程的地址空間執行代碼
擁有資產 資源擁有的基本單位 基本上不佔資源,僅有不可少的資源(程序計數器,一組寄存器和棧)
調度 資源分配的基本單位 獨立調度分配的單位
安全性 進程間相互獨立,互不影響 線程共享一個進程下面的資源,能夠互相通訊和影響
地址空間 系統賦予的獨立的內存地址空間 由相關堆棧寄存器和和線程控制表TCB組成,寄存器可被用來存儲線程內的局部變量

線程切換
cpu給線程分配時間片(也就是分配給線程的時間),執行完時間片後會切換都另外一個線程。

切換以前會保存線程的狀態,下次時間片再給這個線程時才能知道當前狀態。

從保存線程A的狀態再到切換到線程B時,從新加載線程B的狀態的這個過程就叫上下文切換。

而上下切換時會消耗大量的cpu時間。


線程開銷
上下文切換消耗

線程建立和消亡的開銷

線程須要保存維持線程本地棧,會消耗內存


串行,併發與並行
串行
多個任務,執行時一個執行完再執行另外一個。

比喻:吃完飯再看視頻。

併發
多個線程在單個核心運行,同一時間一個線程運行,系統不停切換線程,看起來像同時運行,其實是線程不停切換。

比喻: 一會跑去廚房吃飯,一會跑去客廳看視頻。

並行
每一個線程分配給獨立的核心,線程同時運行。

比喻:一邊吃飯一邊看視頻。


多核下線程數量選擇
計算密集型
程序主要爲複雜的邏輯判斷和複雜的運算。

cpu的利用率高,不用開太多的線程,開太多線程反而會由於線程切換時切換上下文而浪費資源。

IO密集型
程序主要爲IO操做,好比磁盤IO(讀取文件)和網絡IO(網絡請求)。

由於IO操做會阻塞線程,cpu利用率不高,能夠開多點線程,阻塞時能夠切換到其餘就緒線程,提升cpu利用率。

 

基礎補充:

CPU邏輯核心數和物理核心數是什麼意思

一、物理CPU:

物理CPU就是計算機上實際配置的CPU個數。

在linux上能夠打開cat /proc/cpuinfo 來查看,其中的physical id就是每一個物理CPU的ID,能找到幾個physical id就表明計算機實際有幾個CPU。

在linux下能夠經過指令 grep ‘physical id’ /proc/cpuinfo | sort -u | wc -l 來查看物理CPU個數。

二、cpu核數:

linux的cpu核心總數也能夠在/proc/cpuinfo裏面經過指令cat /proc/cpuinfo查看的到,其中的core id指的是每一個物理CPU下的cpu核的id,能找到幾個core id就表明計算機有幾個核心。

也可使用指令cat /proc/cpuinfo | grep 「cpu cores」 | wc -l來統計cpu的核心總數。

三、邏輯CPU:

操做系統可使用邏輯CPU來模擬出真實CPU的效果。在以前沒有多核處理器的時候,一個CPU只有一個核,而如今有了多核技術,其效果就好像把多個CPU集中在一個CPU上。

當計算機沒有開啓超線程時,邏輯CPU的個數就是計算機的核數。而當超線程開啓後,邏輯CPU的個數是核數的兩倍。

實際上邏輯CPU的數量就是平時稱呼的幾核幾線程中的線程數量,在linux的cpuinfo中邏輯CPU數就是processor的數量。

CPU中心那塊隆起的芯片就是核心,是由單晶硅以必定的生產工藝製造出來的。

CPU全部的計算、接受/存儲命令、處理數據都由核心執行,各類CPU核心都具備固定的邏輯結構,一級緩存、二級緩存、執行單元、指令級單元和總線接口等邏輯單元都會有科學的佈局。

 

相關文章
相關標籤/搜索