標籤:

為什麼有人說 Python 的多線程是雞肋呢?


GIL blablabla concurrent blablabla

簡單地說就是作為可能是僅有的支持多線程的解釋型語言(perl的多線程是殘疾,PHP沒有多線程),Python的多線程是有compromise的,在任意時間只有一個Python解釋器在解釋Python bytecode。

UPDATE:如評論指出,Ruby也是有thread支持的,而且至少Ruby MRI是有GIL的。

如果你的代碼是CPU密集型,多個線程的代碼很有可能是線性執行的。所以這種情況下多線程是雞肋,效率可能還不如單線程因為有context switch

但是:如果你的代碼是IO密集型,多線程可以明顯提高效率。例如製作爬蟲(我就不明白為什麼Python總和爬蟲聯繫在一起…不過也只想起來這個例子…),絕大多數時間爬蟲是在等待socket返回數據。這個時候C代碼里是有release GIL的,最終結果是某個線程等待IO的時候其他線程可以繼續執行。

反過來講:你就不應該用Python寫CPU密集型的代碼…效率擺在那裡…

如果確實需要在CPU密集型的代碼里用concurrent,就去用multiprocessing庫。這個庫是基於multi process實現了類multi thread的API介面,並且用pickle部分地實現了變數共享。

再加一條,如果你不知道你的代碼到底算CPU密集型還是IO密集型,教你個方法:

multiprocessing這個module有一個dummy的sub module,它是基於multithread實現了multiprocessing的API。

假設你使用的是multiprocessing的Pool,是使用多進程實現了concurrency

from multiprocessing import Pool

如果把這個代碼改成下面這樣,就變成多線程實現concurrency

from multiprocessing.dummy import Pool

兩種方式都跑一下,哪個速度快用哪個就行了。

UPDATE:

剛剛才發現concurrent.futures這個東西,包含ThreadPoolExecutor和ProcessPoolExecutor,可能比multiprocessing更簡單


有泳池一個,四個泵,但只有一個人,一人只能開啟管理著其中一個,所以四個泵沒什麼用。

====================================================================

但是,如果泵的工作時間與冷卻恢復時間是1:3(感謝inoahx指出,已改),那麼配置的利用率高達100%。(這是基於個人理解的一個比喻,如不妥,請補充)。


多線程還是有用的,多進程有多進程的好處,多線程有多線程的好處。

多進程穩定,啟動時開銷大點,但如果你的運行時間遠大於多進程的時間,用多進程比較方便,如postgresql用多進程,chrome 多進程。

如果你只是想做個定時器樣的簡單東西,對穩定性要求低些,如vb,c#類似的定時器,用多線程吧,但線程的同步要注意了。python的線程更加類似定時器,python的線程不是真線程,但有的場合用這種定時器也能解決很多問題,因為開銷小,開啟也方便。

進程和線程,一個是重量級的,一個輕量級的,重量級的進程有保護區,進程上下文都是操作系統保護的,而線程是自己管理,需要一定的技術,不能保證在並發時的穩定性(多進程也不穩定,但很容易看出來,因為多出了進程容易發現),而python的更像是定時器,定時器有時也可以模擬線程,定時器多時的開銷比線程的開銷要小,真線程有下上文開銷,一個操作系統啟動多進程和多線程會達到切換飽和是有數量的,真線程或進程太多都會導致cpu佔用率居高不下,而定時器可以開n多。

很多東西不是一種比另外一種先進,而是一種互補的關係,計算機的計算單位切換有優點必有缺點,關鍵在找到合適的使用方式揚長避短。


Global Interpretor Lock


在python的原始解釋器CPython中存在著GIL(Global Interpreter Lock,全局解釋器鎖),因此在解釋執行python代碼時,會產生互斥鎖來限制線程對共享資源的訪問,直到解釋器遇到I/O操作或者操作次數達到一定數目時才會釋放GIL。

所以,雖然CPython的線程庫直接封裝了系統的原生線程,但CPython整體作為一個進程,同一時間只會有一個獲得GIL的線程在跑,其他線程則處於等待狀態。這就造成了即使在多核CPU中,多線程也只是做著分時切換而已。

不過muiltprocessing的出現,已經可以讓多進程的python代碼編寫簡化到了類似多線程的程度了。


Python 不適合開發 CPU 密集型的程序,GIL 的作用通俗來講就是,同一時刻一個解釋進程只有一行 bytecode 在執行,那 Python 多線程的意義就是能讓每條語句宏觀上並發執行,而並不代表能提升總體的執行效率,運行時間總和是不減反增的,畢竟多了上下文切換。

當然了,對於 IO 密集型的程序,Python 多線程還是有很大作用的。然而 Python 3 引入的 asyncio 模塊使得很多 IO 操作有了更好的方式去解決,這就非常類似 Node.js 了,都是沒有多線程,而是採用 Event Loop 來處理耗時的 IO 操作。


一般大部分的觀點是由於有 GIL 的存在,Python 中的多線程不能真正的利用多核,不能解決 cpu bound 的問題,但是在一些 IO bound 的程序上卻可以有很好的提升。

但是目前的情況是 我們有了協程啊,在 2.x 系列裡我們可以使用 gevent 啊,在 3.x 系列的標準庫里又有了 asyncio 。IO bound 的問題完全可以用協程解決。而且我們可以自主的控制協程的調度了。為什麼還要使用由 OS 調度的不太可控的線程呢?

所以我認為線程在 Python 里就是個雞肋。尤其實在 3.x 系列裡。

還有一個介紹 2012 不宜進入的三個技術點(中)


因為Python的多線程不能調用多個核心,只能利用一個核心. 如果是IO密集帶阻塞的任務,Python的多線程還是很不錯的. 如果是CPU密集, 試試多進程好了.

試一試這個多進程:

http://www.parallelpython.com/?


對於計算密集型的任務,多線程是雞肋,不如用多進程,但是對於IO密集型的任務,多線程並不是雞肋,因為網路IO的延遲比CPU的更大。


在介紹Python中的線程之前,先明確一個問題,Python中的多線程是假的多線程! 為什麼這麼說,我們先明確一個概念,全局解釋器鎖(GIL)。

Python代碼的執行由Python虛擬機(解釋器)來控制。Python在設計之初就考慮要在主循環中,同時只有一個線程在執行,就像單CPU的系統中運行多個進程那樣,內存中可以存放多個程序,但任意時刻,只有一個程序在CPU中運行。同樣地,雖然Python解釋器可以運行多個線程,只有一個線程在解釋器中運行。

對Python虛擬機的訪問由全局解釋器鎖(GIL)來控制,正是這個鎖能保證同時只有一個線程在運行。在多線程環境中,Python虛擬機按照以下方式執行。

1.設置GIL。

2.切換到一個線程去執行。

3.運行。

4.把線程設置為睡眠狀態。

5.解鎖GIL。

6.再次重複以上步驟。

對所有面向I/O的(會調用內建的操作系統C代碼的)程序來說,GIL會在這個I/O調用之前被釋放,以允許其他線程在這個線程等待I/O的時候運行。如果某線程並未使用很多I/O操作,它會在自己的時間片內一直佔用處理器和GIL。也就是說,I/O密集型的Python程序比計算密集型的Python程序更能充分利用多線程的好處。

我們都知道,比方我有一個4核的CPU,那麼這樣一來,在單位時間內每個核只能跑一個線程,然後時間片輪轉切換。但是Python不一樣,它不管你有幾個核,單位時間多個核只能跑一個線程,然後時間片輪轉。看起來很不可思議?但是這就是GIL搞的鬼。任何Python線程執行前,必須先獲得GIL鎖,然後,每執行100條位元組碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把所有線程的執行代碼都給上了鎖,所以,多線程在Python中只能交替執行,即使100個線程跑在100核CPU上,也只能用到1個核。通常我們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。

我們不妨做個試驗:

#coding=utf-8
from multiprocessing import Pool
from threading import Thread

from multiprocessing import Process

def loop():
while True:
pass

if __name__ == "__main__":

for i in range(3):
t = Thread(target=loop)
t.start()

while True:
pass

我的電腦是4核,所以我開了4個線程,看一下CPU資源佔有率:

我們發現CPU利用率並沒有佔滿,大致相當於單核水平。

而如果我們變成進程呢?

我們改一下代碼:

#coding=utf-8
from multiprocessing import Pool
from threading import Thread

from multiprocessing import Process

def loop():
while True:
pass

if __name__ == "__main__":

for i in range(3):
t = Process(target=loop)
t.start()

while True:
pass

結果直接飆到了100%,說明進程是可以利用多核的!

為了驗證這是Python中的GIL搞得鬼,我試著用Java寫相同的代碼,開啟線程,我們觀察一下:

package com.darrenchan.thread;

public class TestThread {
public static void main(String[] args) {
for (int i = 0; i &< 3; i++) { new Thread(new Runnable() { @Override public void run() { while (true) { } } }).start(); } while(true){ } } }

由此可見,Java中的多線程是可以利用多核的,這是真正的多線程!而Python中的多線程只能利用單核,這是假的多線程!

難道就如此?我們沒有辦法在Python中利用多核?當然可以!剛才的多進程算是一種解決方案,還有一種就是調用C語言的鏈接庫。對所有面向I/O的(會調用內建的操作系統C代碼的)程序來說,GIL會在這個I/O調用之前被釋放,以允許其他線程在這個線程等待I/O的時候運行。我們可以把一些 計算密集型任務用C語言編寫,然後把.so鏈接庫內容載入到Python中,因為執行C代碼,GIL鎖會釋放,這樣一來,就可以做到每個核都跑一個線程的目的!

可能有的小夥伴不太理解什麼是計算密集型任務,什麼是I/O密集型任務?

計算密集型任務的特點是要進行大量的計算,消耗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語言最差。

綜上,Python多線程相當於單核多線程,多線程有兩個好處:CPU並行,IO並行,單核多線程相當於自斷一臂。所以,在Python中,可以使用多線程,但不要指望能有效利用多核。如果一定要通過多線程利用多核,那隻能通過C擴展來實現,不過這樣就失去了Python簡單易用的特點。不過,也不用過於擔心,Python雖然不能利用多線程實現多核任務,但可以通過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。

分享廖雪峰的博客:廖雪峰博客


不能做到並行,但可以做到並發。

對於計算密集型應用,python的多線程確實沒啥用。

但對於向網頁提交多個request這種IO密集型應用,並發就很有用了。


最近在使用python寫推薦系統(前期先用python測試效果,線上用java)
在計算相似度矩陣的時候,python的多線程大大減少了計算時間,同時,機器的cpu使用率也從30%漲到100%(不過cpu中斷和上下文切換數也很高)。

計算相似度矩陣是一個cpu密集型任務,具體來說是一個6000*20000的矩陣(喜愛度矩陣)與其轉置相乘,得到一個6000*6000的矩陣。這個運算涉及17994000次向量運算,每次運算是兩個20000維向量求點積。

我先建立一個6000*6000的0矩陣,而後將這將近1800萬次運算分為4部分,建立四個線程來計算。每個線程每計算完一次向量點積,便更新0矩陣中對應位置的數字。

整體來說,使用多線程後,運算速度大大加快,大概為單線程的一半左右。 @yegle

------------------------------

別光點反對啊,有什麼問題可以回復一下,一起討論。

我知道python有GIL,同時只有一個進程在運行。但是這種方法確實提高了計算速度,不知是否有大神可以幫忙解釋下。


@yegle 說的大部分都是對的。

唯有並發 I/O 的一部分…… 為什麼會有「線程是為不懂狀態機的程序員準備的」這種說法?在單核的計算機上編程根本不需要使用多線程編程嗎? 這個問題已經說了我想說的了。

並發 I/O 的情況請善用 Tornado / Gevent 這種基於庫,每 CPU 核心起一個進程跑一個 event loop;除非請求的處理時間遠大於 I/O 和 job scheduling 的時間,這種情況實際上也應該通過 MQ 往多機上發布任務了。


我覺得為什麼在Python里推薦使用多進程而不是多線程?這篇文章解釋的很清楚,主要還是Python本身的GIL機制帶來的問題。


Python多線程是不是雞肋,是,GIL那個東西再那裡擺著,就算在多核下面Python也是無法並行的,這個好理解嘛,就相當於做了個分時復用。

Python多線程有沒有用,有,你去爬圖片站的時候,用單進程單線程這種方式,進程很容易阻塞在獲取數據socket函數上,多線程可以緩解這種情況。你說解決沒有,要是每個請求都阻塞起了,那多線程也沒什麼用(當然,這種情況沒見過哈)。

Python的優勢就在於寫起來快,用起來方便。你要做計算密集型的,還想並行化的話,還是用C吧。


個人看法,線程本身就是一個有些複雜甚至可以說有些醜陋的解決方案,95%的情況下其實都可以不用——KISS。

如果要用並發處理,基本上使用 非阻塞多路復用+多進程 方式,就可以處理絕大多數需求了


推薦閱讀:

如何讓自己的 python 代碼更有逼格?
使用Python建站對於日流量20w的pv你會採用什麼樣的架構?

TAG:Python |