(現在人工智能非常火爆,很多朋友都想學,但是一般的教程都是為博碩生準備的,太難看懂了。最近發現了一個非常適合小白入門的教程,不僅通俗易懂而且還很風趣幽默。所以忍不住分享一下給大家。
? 點這里https://www.cbedai.net/ialexanderi可以跳轉到教程。)
說明
進程:是操作系統進行資源分配的最小單元,資源包括CPU、內存、磁盤等IO設備等等
線程:是CPU調度的基本單位。
進程:系統分配資源的載體,是程序運行的實例;
線程:程序執行的最小單元,是進程中的一個實體用來執行程序,一個進程中有多個線程。
為什么有人說 Python 多線程是雞肋?
?
在我們常識中,多進程、多線程都是通過并發的方式充分利用硬件資源提高程序的運行效率,怎么在 Python 中反而成了雞肋?
因為 Python 中臭名昭著的 GIL。
那么 GIL 是什么?為什么會有 GIL?多線程真的是雞肋嗎? GIL 可以去掉嗎?
多線程是不是雞肋:
做個實驗:
將數字 “1億” 遞減,減到 0 程序就終止,這個任務如果我們使用單線程來執行,完成時間會是多少?
單線程,4核 CPU 計算機中,單線程所花的時間是 6.5 秒。
多線程創建兩個子線程 t1、t2,每個線程各執行 5 千萬次減操作,兩個線程以合作的方式執行是 6.8 秒,反而變慢了。
?
按理來說,兩個線程同時并行地運行,時間應該不減反增。原因就在于 GIL ,
在Python多線程下,每個線程的執行方式:
1.獲取GIL
2.執行代碼直到sleep或者是python虛擬機將其掛起。
3.釋放GIL
可見,某個線程想要執行,必須先拿到GIL,我們可以把GIL看作是“通行證”,并且在一個python進程中,GIL只有一個。拿不到通行證的線程,就不允許進入CPU執行。
?
?
在 Cpython 解釋器(Python語言的主流解釋器)中,有一把全局解釋鎖(Global Interpreter Lock),在解釋器解釋執行 Python 代碼時,先要得到這把鎖,意味著,任何時候只可能有一個線程在執行代碼,其它線程要想獲得 CPU 執行代碼指令,就必須先獲得這把鎖,如果鎖被其它線程占用了,那么該線程就只能等待,直到占有該鎖的線程釋放鎖才有執行代碼指令的可能。
同一時刻,只有一個線程在運行,其它線程只能等待,即使是多核CPU,也沒辦法讓多個線程「并行」地同時執行代碼,只能是交替執行,因為多線程涉及到上線文切換、鎖機制處理(獲取鎖,釋放鎖等),所以,多線程執行不快反慢。
什么時候 GIL 被釋放呢?
當一個線程遇到 I/O 任務時,將釋放GIL 。 計算密集型(CPU-bound)線程執行 100 次解釋器的計步(ticks)時(計步可粗略看作 Python 虛擬機的指令),也會釋放 GIL。
可以通過設置計步長度,查看計步長度。相比單線程,這些多是多線程帶來的額外開銷。
???????CPython 解釋器為什么要這樣設計?多線程有個問題,怎么解決共享數據的同步、一致性問題,因為,對于多個線程訪問共享數據時,可能有兩個線程同時修改一個數據情況,如果沒有合適的機制保證數據的一致性,那么程序最終導致異常,所以,Python之父就搞了個全局的線程鎖,不管你數據有沒有同步問題,反正一刀切,上個全局鎖,保證數據安全。這也就是多線程雞肋的原因,因為它沒有細粒度的控制數據的安全,而是用一種簡單粗暴的方式來解決。
??????這種解決辦法放在90年代,其實是沒什么問題的,畢竟,那時候的硬件配置還很簡陋,單核 CPU 還是主流,多線程的應用場景也不多,大部分時候還是以單線程的方式運行,單線程不要涉及線程的上下文切換,效率反而比多線程更高(在多核環境下,不適用此規則)。
??????所以,采用 GIL 的方式來保證數據的一致性和安全,未必不可取,至少在當時是一種成本很低的實現方式。那么把 GIL 去掉可行嗎?還真有人這么干多,但是結果令人失望,在1999年Greg Stein 和Mark Hammond 兩位哥們就創建了一個去掉 GIL 的 Python 分支,在所有可變數據結構上把 GIL 替換為更為細粒度的鎖。然而,做過了基準測試之后,去掉GIL的 Python 在單線程條件下執行效率將近慢了2倍。
Python之父表示:基于以上的考慮,去掉GIL沒有太大的價值而不必花太多精力。小結CPython解釋器提供 GIL 保證線程數據同步,那么有了 GIL,我們還需要線程同步嗎?多線程在 IO 密集型任務中,表現又是怎樣呢?歡迎大家留言。
?
python的多線程到底有沒有用?
?
?
1、CPU密集型代碼(各種循環處理、計數等等),在這種情況下,
ticks計數很快就會達到閾值,然后觸發GIL的釋放與再競爭(多個線程來回切換當然是需要消耗資源的),所以python下的多線程對CPU密集型代碼并不友好。
2、IO密集型代碼(文件處理、網絡爬蟲等),多線程能夠有效提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序執行效率)。
所以python的多線程對IO密集型代碼比較友好。
而在python3.x中,GIL不使用ticks計數,改為使用計時器(執行時間達到閾值后,當前線程釋放GIL),這樣對CPU密集型程序更加友好,但依然沒有解決GIL導致的同一時間只能執行一個線程的問題,所以效率依然不盡如人意。
多核多線程比單核多線程更差,原因是單核下多線程,每次釋放GIL,喚醒的那個線程都能獲取到GIL鎖,所以能夠無縫執行,但多核下,CPU0釋放GIL后,其他CPU上的線程都會進行競爭,但GIL可能會馬上又被CPU0拿到,導致其他幾個CPU上被喚醒后的線程會醒著等待到切換時間后又進入待調度狀態,這樣會造成線程顛簸(thrashing),導致效率更低
回到最開始的問題:經常我們會聽到老手說:“python下想要充分利用多核CPU,就用多進程”,原因是什么呢?
原因是:每個進程有各自獨立的GIL,互不干擾,這樣就可以真正意義上的并行執行,所以在python中,多進程的執行效率優于多線程(僅僅針對多核CPU而言)。
所以在這里說結論:多核下,想做并行提升效率,比較通用的方法是使用多進程,能夠有效提高執行效率
?
話說回來,CPU密集型的程序用python來做,本身就不合適。跟C,Go,Java的速度比,實在性能差到沒法說。你當然可以寫個C擴展來實現真正的多線程,用python來調用,那樣速度是快。我們之所以用python來做,只是因為開發效率超高,可以快速實現。
最后補充幾點:
python中要想利用好CPU,還是用多進程來做吧。或者,可以使用協程。multiprocessing和gevent在召喚你。
GIL不是bug,Guido也不是水平有限才留下這么個東西。龜叔曾經說過,嘗試不用GIL而用其他的方式來做線程安全,結果python語言整體效率又下降了一倍,權衡利弊,GIL是最好的選擇——不是去不掉,而是故意留著的。
想讓python計算速度快起來,又不想寫C?用pypy吧,這才是真正的大殺器。
?
不適合用多線程的情況下用多進程還是協程提高并發能力?
一、多進程能夠更好的利用多核CPU。
????但是多進程也有其自己的限制:相比線程更加笨重、切換耗時更長,并且在python的多進程下,進程數量不推薦超過CPU核心數(一個進程只有一個GIL,所以一個進程只能跑滿一個CPU),因為一個進程占用一個CPU時能充分利用機器的性能,但是進程多了就會出現頻繁的進程切換,反而得不償失。
所以多核的情況下,考慮線程數與 CPU核心數相同的多線程,充分利用CPU的多核能力。
?
二、什么時候需要協程?
不過特殊情況(特指IO密集型任務)下,多線程是比多進程好用的。
舉個例子:給你200W條url,需要你把每個url對應的頁面抓取保存起來,這種時候,單單使用多進程,效果肯定是很差的。為什么呢?
例如每次請求的等待時間是2秒,那么如下(忽略cpu計算時間):
1、單進程+單線程:需要2秒*200W=400W秒==1111.11個小時==46.3天,這個速度明顯是不能接受的
2、單進程+多線程:例如我們在這個進程中開了10個多線程,比1中能夠提升10倍速度,也就是大約4.63天能夠完成200W條抓取,請注意,這里的實際執行是:線程1遇見了阻塞,CPU切換到線程2去執行,遇見阻塞又切換到線程3等等,10個線程都阻塞后,這個進程就阻塞了,而直到某個線程阻塞完成后,這個進程才能繼續執行,所以速度上提升大約能到10倍(這里忽略了線程切換帶來的開銷,實際上的提升應該是不能達到10倍的),但是需要考慮的是線程的切換也是有開銷的,所以不能無限的啟動多線程(開200W個線程肯定是不靠譜的)
3、多進程+多線程:這里就厲害了,一般來說也有很多人用這個方法,多進程下,每個進程都能占一個cpu,而多線程從一定程度上繞過了阻塞的等待,所以比單進程下的多線程又更好使了,例如我們開10個進程,每個進程里開20W個線程,執行的速度理論上是比單進程開200W個線程快10倍以上的(為什么是10倍以上而不是10倍,主要是cpu切換200W個線程的消耗肯定比切換20W個線程進程大得多,考慮到這部分開銷,所以是10倍以上)。
還有更好的方法嗎?答案是肯定的,它就是:
4、協程,使用它之前我們先講講what/why/how(它是什么/為什么用它/怎么使用它)
?
what:
協程是一種用戶級的輕量級線程。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此:
協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合), 每次過程重入時,就相當于進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。
在并發編程中,協程與線程類似,每個協程表示一個執行單元,有自己的本地數據,與其它協程共享全局數據和其它資源。
why:
目前主流語言基本上都選擇了多線程作為并發設施,與線程相關的概念是搶占式多任務(Preemptive multitasking),而與協程相關的是協作式多任務。
不管是進程還是線程,每次阻塞、切換都需要陷入系統調用(system call),先讓CPU跑操作系統的調度程序,然后再由調度程序決定該跑哪一個進程(線程)。
而且由于搶占式調度執行順序無法確定的特點,使用線程時需要非常小心地處理同步問題,而協程完全不存在這個問題(事件驅動和異步程序也有同樣的優點)。
因為協程是用戶自己來編寫調度邏輯的,對CPU來說,協程其實是單線程,所以CPU不用去考慮怎么調度、切換上下文,這就省去了CPU的切換開銷,所以協程在一定程度上又好于多線程。
how:
python里面怎么使用協程?答案是使用gevent,使用方法:看這里
使用協程,可以不受線程開銷的限制,我嘗試過一次把20W條url放在單進程的協程里執行,完全沒問題。
所以最推薦的方法,是多進程+協程(可以看作是每個進程里都是單線程,而這個單線程是協程化的)
多進程+協程下,避開了CPU切換的開銷,又能把多個CPU充分利用起來,這種方式對于數據量較大的爬蟲還有文件讀寫之類的效率提升是巨大的。
小例子:
?
#-*- coding=utf-8 -*-
?
import requests
?
from multiprocessing import Process
?
import gevent
?
from gevent import monkey; monkey.patch_all()
?
?
?
import sys
?
reload(sys)
?
sys.setdefaultencoding('utf8')
?
def fetch(url):
?
? ? try:
?
? ? ? ? s = requests.Session()
?
? ? ? ? r = s.get(url,timeout=1)#在這里抓取頁面
?
? ? except Exception,e:
?
? ? ? ? print e?
?
? ? return ''
?
?
?
def process_start(url_list):
?
? ? tasks = []
?
? ? for url in url_list:
?
? ? ? ? tasks.append(gevent.spawn(fetch,url))
?
? ? gevent.joinall(tasks)#使用協程來執行
?
?
?
def task_start(filepath,flag = 100000):#每10W條url啟動一個進程
?
? ? with open(filepath,'r') as reader:#從給定的文件中讀取url
?
? ? ? ? url = reader.readline().strip()
?
? ? ? ? url_list = []#這個list用于存放協程任務
?
? ? ? ? i = 0 #計數器,記錄添加了多少個url到協程隊列
?
? ? ? ? while url!='':
?
? ? ? ? ? ? i += 1
?
? ? ? ? ? ? url_list.append(url)#每次讀取出url,將url添加到隊列
?
? ? ? ? ? ? if i == flag:#一定數量的url就啟動一個進程并執行
?
? ? ? ? ? ? ? ? p = Process(target=process_start,args=(url_list,))
?
? ? ? ? ? ? ? ? p.start()
?
? ? ? ? ? ? ? ? url_list = [] #重置url隊列
?
? ? ? ? ? ? ? ? i = 0 #重置計數器
?
? ? ? ? ? ? url = reader.readline().strip()
?
? ? ? ? if url_list not []:#若退出循環后任務隊列里還有url剩余
?
? ? ? ? ? ? p = Process(target=process_start,args=(url_list,))#把剩余的url全都放到最后這個進程來執行
?
? ? ? ? ? ? p.start()
?
??
?
if __name__ == '__main__':
?
? ? task_start('./testData.txt')#讀取指定文件
?
?
細心的同學會發現:上面的例子中隱藏了一個問題:進程的數量會隨著url數量的增加而不斷增加,我們在這里不使用進程池multiprocessing.Pool來控制進程數量的原因是multiprocessing.Pool和gevent有沖突不能同時使用,但是有興趣的同學可以研究一下gevent.pool這個協程池。
?
參考:https://cloud.tencent.com/developer/news/218164 《為什么有人說 Python 多線程是雞肋?》
??????????https://www.cnblogs.com/anpengapple/p/6014480.html 《python的多線程到底有沒有用? 》
????????https://blog.csdn.net/lambert310/article/details/50605748 《談談python的GIL、多線程、多進程》
————————————————
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
