關於 Python 多線程的一些思考
作者:許鍵
來源:騰訊雲技術社區(https://www.qcloud.com/community/)
導語
在知乎等地方經常看到有人問,Python 的多線程是不是雞肋?為何我用多線程性能一點沒有提升,有時候性能反而下降?在這裡通過日常工作中遇到的問題以及自己的一些總結,來一探 Python 多線程究竟是不是雞肋;如果不是,那又該如何使用。
1、遇到的問題
工作中常用到 python 來分析文件,統計數據;隨著業務的發展,原先的代碼性能受到了一定的挑戰,下面根據兩個案例來講解在 python 的使用過程中,遇到的一些問題,以及自己的一些總結。
案例一:數據統計,將文件按照一定的邏輯統計匯總後,入庫到本地 db 中。
最開始的代碼流程框圖:
大概流程:
1、循環讀文件,按照一定格式將文本進行拆分計算;
2、根據指定的 key 來統一匯總數據;
3、入庫本地 DB,入庫時,會先查找 db 中是否存在這條記錄,然後再判斷是否插入 db 中;相當於這裡會有兩次 db 操作,一次查詢,一次寫入。
一開始業務量小,db 數據量少,整個流程耗時較短,在秒級能夠完成,隨著業務發展,所需時間也有秒級變成了分鐘級,十分鐘級別等。
測試 1
通過對其中的一個業務某天的數據進行測試,它的耗時主要分部為,讀文件(文件大小 1G 左右)耗時 2 秒,邏輯計算及匯總 58s,數據入庫 32s;總耗時大概在 1 分半鐘。
1.1 方案一
流水線形式的多線程
線程1負責讀取數據,然後通過 python 自帶的 Qeueue,Q1 傳遞數據給線程 2;
線程 2 負責邏輯計算,然後通過 Q2 傳遞數據給線程 3;
線程 3 負責匯總數據,然後通過 Q3 傳遞數據給線程 4;
線程 4 則入庫數據;
這個方案在實現之後立馬就被廢棄了,它的效率比單進程的效率低很多,通過查看系統調用之後,發現是因為多個線程一直在競爭鎖,以及線程切換導致其執行效率還不如單線程。
1.2 方案二
數據分片分段多線程
在不同的時機採用多線程來處理,同時盡量避免多線程對同一資源進行競爭,以減少鎖的切換帶來的消耗。因此這裡在邏輯計算和數據入庫階段分別採用數據分組,多線程執行的方式來進行處理。
分段一、邏輯計算和匯總,將內存讀到內存中後,按照線程數量,將數據切分成多塊,讓第 i 個線程 thread[i]處理第 i 份數據 data[i],最後再將計算得到的 4 份數據匯總,按照相同的 key 進行匯總;得到 sum_data。
分段二、將 sumdata 按照線程數量,切分成多份,同樣讓 thread[i]處理 sumdata[i]的數據,讓他們各自進行數據的查詢以及寫入操作。
測試 2
對同一個文件進行測試,讀文件耗時 2 秒,邏輯計算及匯總 62s,數據入庫 10s;總耗時大概在 70 多秒,相比最開始的單線程,時間大概下降了 20 多 s;但可以看出,邏輯計算的時間相比單線程確增加了不少,而入庫操作的時間減少了 20 多 s;這裡就引發一個問題,邏輯計算跟入庫的差異在哪裡?為何前面的多線程性能下降,而後面性能確大幅度提高。
這裡的原因究竟為何?
案例二
案例 2 的整體流程為,將幾份不同的數據源從 db 中取出來,按天取出,經過一定的整合後,匯總插入到一個目標 db 中。
一開始的代碼流程:
同樣最初的時候,需要整合的數據量較少,db 中的數據量也較少。隨著業務增長,每天需要處理的數據量也逐漸增加,並且 db 中的數據量也越來越大,處理的時間也從開始的秒級別也逐漸增加到分鐘級別,每次都是統一處理一個月的數據,整體耗時需要幾個小時。
測試 3
對某天的數據進行測試,結果為:取數據 整合 耗時 30s;插入數據耗時約 8 分鐘。
更改成以下模型:
入庫操作同樣需要先根據 key 查找當前 db 中是否存在該條數據,不存在則寫入。
測試 4
取數據 整合 耗時 30s;插入數據耗時約 2 分鐘。
更改之後性能大幅度提升,由原先的 8 分半鐘,縮減為不到 2 分半鐘左右,縮減的時間主要體現在入庫階段;
從以上兩個例子可以看到,當涉及 I/0 操作時,python 的多線程能發揮較好的性能;而當涉及到 CPU 密集型邏輯運算時,python 的多線程性能不升反降。這裡都是由於 python 的 GIL 在發揮的作用。
2、了解 python 的 GIL
這裡我們使用的解釋器為官方實現的 CPython,它是由 C 語言實現的 python 解釋器。同時還存在由 Java 實現的 Jython 解釋器,由.NET 實現的 IronPython 等解釋器。這裡我們主要是依據 CPython 來講解 GIL 鎖。
GIL,全稱 Global Interpreter Lock, 是一個全局解釋器鎖,當解釋器需要執行 python 代碼時,都必須要獲得這把鎖,然後才能執行。當解釋器在遇到到 I/O 操作時會釋放這把鎖。但如果當程序為 CPU 密集型時,解釋器會主動的每間隔 100 次操作就釋放這把 GIL 鎖,這樣別的線程有機會執行,具體的間隔次數是由 sys.setcheckinterval( number ) 來設定這個值,通過 sys.getcheckinterval() 返回這個值,默認為 100。所以,儘管 Python 的線程庫直接封裝操作系統的原生線程,但 Python 進程,在同一時間只會有一個獲得了 GIL 的線程在跑,其它的線程都處於等待狀態等著 GIL 的釋放。就這樣對於 CPU 密集型操作來說,多線程不但不會提升性能,還會因為線程切換,鎖競爭等導致性能的下降。
在我們上面的兩個例子中,當涉及到數據的查詢與插入時,都需要進行 I/O 交互,並且會等待資料庫伺服器返回,這個時候,線程會主動釋放鎖,其他線程能夠合理利用這個時間,來做同樣的事情。
知道了 GIL 之後,我們才能更加合理的使用 python 的多線程,並不是所有場景都適用於多線程。
同樣,Python 的多線程也並不是大家所說的雞肋,在適合的場景用上了,還是能夠起到驚艷的作用。
題圖:pexels,CC0 授權。
點擊展開全文


※免費直播教你如何零基礎入門Python Web?
※Python 菜鳥教程
※讓小烏龜可以唱歌——對Python turtle進行拓展!祝全天下的父親們父親節快樂
※Python 編寫知乎爬蟲實踐
TAG:Python |
※android 多線程編程
※python threading中處理主進程和子線程的關係
※PyQt5+ Python3 多線程通信
※muduo——EventLoop處理線程安全的問題
※高性能的 PHP 封裝的 HTTP Restful 多線程並發請求庫-MultiHttp
※線程池ThreadPoolExecutor里Control state,ctl參數的分析(一)
※線程池ThreadPoolExecutor里Control state,ctl參數的分析(二)
※python筆記12-python多線程之事件
※Python學習之進程和線程
※python udp的應用 ,多線程實現聊天功能
※風冷也能壓制線程撕裂者,Arctic Cooling展示Freezer 50 TR散熱器
※Synchronized鎖在Spring事務管理下,為啥還線程不安全?
※你懂ThreadPoolExecutor線程池技術嗎?看源碼你會有全新的認識
※入門Python多線程/多進程編程
※Android 進程和線程
※python爬取youtube視頻 多線程 非中文自動翻譯
※Jmeter如何設置線程數,ramp-up period,循環次數
※Python並發:線程和鎖
※由於ZombieLoad漏洞,英特爾的CPU超線程可能要涼了
※SqlSessionTemplate是如何保證MyBatis中SqlSession的線程安全的?