python多線程、多進程 初探
原先剛學Java的時候,多線程也學了幾天,后來一直沒用到。然后接觸python的多線程的時候,貌似看到一句”python多線程很雞肋“,於是乎直接跳過了多線程的學習。
接觸爬蟲,才開始用到多進程這個東西。
既然用到了,就系統地學吧。先來python的,再總結一下Java的。
什么是線程和進程
很經典的一個解釋是“進程是資源分配的最小單位,線程是CPU調度的最小單位“。
比如我們在任務管理器中看到的就是進程,例如qq.exe,qq就是一個進程。打開qq后,啟動多條線程,各自負責文件下載、更新朋友圈、聊條等功能。
每個進程,都有自己的地址空間、內存、數據棧以及其他用於跟蹤執行的輔助數據。進程可以通過派生(fork或spawn)新的進程來執行其他任務,不過這個新的進程是獨立的,所有進程之間只能采用IPC(進程間通信)的方式共享信息。
線程,於進程類似,有時候也稱為輕量級進程,可以把他們 視為一個主進程中並行運行的一些”迷你進程“。同一個進程下的線程共享資源。
以前單核CPU時代,多線程用處並不多。進程和線程沒法做到真正的並行運算,而如今多核時代,真正能實現多條線程一起跑。
線程於進程的對比
對比維度 | 進程 | 線程 |
---|---|---|
數據共享、同步 | IPC共享數據較復雜,同步簡單 | 同進程下線程數據共享,但同步復雜 |
可靠性 | 進程間互相獨立 | 一個線程壞掉可能使整個進程掛掉 |
資源消耗 | 獨立資源,消耗多。進程間切換慢 | 切換快,共享資源,資源消耗少 |
總結,進程和線程還可以類比為火車和車廂:
線程在進程下行進(單純的車廂無法運行)
一個進程可以包含多個線程(一輛火車可以有多個車廂)
不同進程間數據很難共享(一輛火車上的乘客很難換到另外一輛火車,比如站點換乘)
同一進程下不同線程間數據很易共享(A車廂換到B車廂很容易)
進程要比線程消耗更多的計算機資源(采用多列火車相比多個車廂更耗資源)
進程間不會相互影響,一個線程掛掉將導致整個進程掛掉(一列火車不會影響到另外一列火車,但是如果一列火車上中間的一節車廂着火了,將影響到該趟火車的所有車廂)
進程可以拓展到多機,進程最多適合多核(不同火車可以開在多個軌道上,同一火車的車廂不能在行進的不同的軌道上)
進程使用的內存地址可以上鎖,即一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。(比如火車上的洗手間)-”互斥鎖(mutex)”
進程使用的內存地址可以限定使用量(比如火車上的餐廳,最多只允許多少人進入,如果滿了需要在門口等,等有人出來了才能進去)-“信號量(semaphore)”
p ython的多線程為何雞肋
因為GIL,全局解釋鎖機制,python的多線程是偽多線程。
准確來說,GIL並不是python的機制,而是CPython解釋器(主流解釋器)的機制(比如Jpython就沒有GIL)。
什么是GIL
其實有點類似於單核CPU執行多進程的過程,雖然可以有多個進程,但只有一個CPU啊,一個進程執行的時候別的進程只能等,只是快速切換執行感覺上是一起執行罷了。現在多核CPU能做到真正的並行
在 Cpython 解釋器(Python語言的主流解釋器)中,有一把全局解釋鎖(Global Interpreter Lock),在解釋器解釋執行 Python 代碼時,先要得到這把鎖。鎖只有一把,一個線程執行的時候,別的線程也得等,多核CPU也沒用。(真正的多線程,比如Java的多線程,在多核情況下,可以實現真正的多條線程同步運行)
1.設置GIL
2.切換到一個線程去執行
3.運行
指定數量的字節碼指令
線程主動讓出控制(可以調用time.sleep(0))
4.把線程設置完睡眠狀態
5.解鎖GIL
6.再次重復以上步驟
線程什么時候會讓出全局鎖呢?一是執行滿 100 tick(可以理解為100單位的指令),二是遇見了阻塞(比如I/O)。
考慮兩種情況
- 計算密集:這時,幾乎沒一個線程都是執行滿100tick后讓出鎖的,所以和單線程跑沒啥區別,而且線程切換也需要時間,最終程序運行時間比起單線程反而更長。
- I/O密集型:一個線程只執行了幾tick,遇見I/O阻塞。這時候代碼已經解釋過了,可以獨立執行I/O過程,這是切換到另一個線程,同理。最終類似於真正的多線程,在I/O上節省的時間多余在線程切換消耗的時間,比單線程執行快。
因而,python多線程適合I/O密集型程序。
CPython 為什么要這樣設計
多線程有個問題,怎么解決共享數據的同步、一致性問題?因為,對於多個線程訪問共享數據時,可能有兩個線程同時修改一個數據情況,如果沒有合適的機制保證數據的一致性,那么程序最終導致異常,所以,Python之父就搞了個全局的線程鎖,不管你數據有沒有同步問題,反正一刀切,上個全局鎖,保證數據安全,簡單粗暴。
這種解決辦法放在90年代,其實是沒什么問題的,畢竟,那時候的硬件配置還很簡陋,單核 CPU 還是主流,多線程的應用場景也不多,大部分時候還是以單線程的方式運行,單線程不要涉及線程的上下文切換,效率反而比多線程更高(在多核環境下,不適用此規則)。所以,采用 GIL 的方式來保證數據的一致性和安全,未必不可取,至少在當時是一種成本很低的實現方式。
解決方案
因為Cpython的GIL,使python的多線程顯得雞肋。可以試着使用別的解釋器,但更常見的作法還是使用多進程。(當然,多進程必然消耗資源多一點)。
接下來學習
多線程主要用的是標准庫中的threading包,thread基本不用(主要是它在主線程結束后,其他線程會直接停止,有點類似於守護進程,顯然不安全)。
多進程使用標准庫中的multiprocessing。
接下來幾天閱讀官方文檔中的這幾節。