多線程真的比單線程快?
事實(shí)上,Python 多線程另一個(gè)很重要的話題叫,GIL(Global Interpreter Lock,即全局解釋器鎖)。
多線程不一定比單線程快
在Python中,可以通過多進(jìn)程、多線程和多協(xié)程來實(shí)現(xiàn)多任務(wù)。難道多線程就一定比單線程快?
下面我用一段代碼證明我自己得觀點(diǎn)。
- '''
- @Author:Runsen
- @微信公眾號(hào):Python之王
- @博客:https://blog.csdn.net/weixin_44510615
- @Date:2020/6/4
- '''
- import threading, time
- def my_counter():
- i = 0
- for _ in range(100000000):
- i = i+1
- return True
- def main1():
- start_time = time.time()
- for tid in range(2):
- t = threading.Thread(target=my_counter)
- t.start()
- t.join() # 第一次循環(huán)的時(shí)候join方法引起主線程阻塞,但第二個(gè)線程并沒有啟動(dòng),所以兩個(gè)線程是順序執(zhí)行的
- print("單線程順序執(zhí)行total_time: {}".format(time.time() - start_time))
- def main2():
- thread_ary = {}
- start_time = time.time()
- for tid in range(2):
- t = threading.Thread(target=my_counter)
- t.start()
- thread_ary[tid] = t
- for i in range(2):
- thread_ary[i].join() # 兩個(gè)線程均已啟動(dòng),所以兩個(gè)線程是并發(fā)的
- print("多線程執(zhí)行total_time: {}".format(time.time() - start_time))
- if __name__ == "__main__":
- main1()
- main2()
運(yùn)行結(jié)果
- 單線程順序執(zhí)行total_time: 17.754502773284912
- 多線程執(zhí)行total_time: 20.01178550720215
我怕你說我亂得出來得結(jié)果,我還是截個(gè)圖看清楚點(diǎn)
這時(shí),我懷疑:我的機(jī)器出問題了嗎?其實(shí)不是這樣,本質(zhì)上來說Python 的線程失效了,沒有起到并行計(jì)算的作用。
Python 的線程,的確封裝了底層的操作系統(tǒng)線程,在 Linux 系統(tǒng)里是 Pthread(全稱為 POSIX Thread),而在 Windows 系統(tǒng)里是 Windows Thread。另外,Python 的線程,也完全受操作系統(tǒng)管理,比如協(xié)調(diào)何時(shí)執(zhí)行、管理內(nèi)存資源、管理中斷等等。
GIL不是Python的特性
GIL 的概念用簡單的一句話來解釋,就是「任一時(shí)刻,無論線程多少,單一 CPython 解釋器只能執(zhí)行一條字節(jié)碼」。這個(gè)定義需要注意的點(diǎn):
首先需要明確的一點(diǎn)是「GIL并不是Python的特性」,它是在實(shí)現(xiàn)Python解析器(CPython)時(shí)所引入的一個(gè)概念。
C++是一套語言(語法)標(biāo)準(zhǔn),但是可以用不同的編譯器來編譯成可執(zhí)行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。
Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執(zhí)行環(huán)境來執(zhí)行。
「其他 Python 解釋器不一定有 GIL」。例如 Jython (JVM) 和 IronPython (CLR) 沒有 GIL,而 CPython,PyPy 有 GIL;
因?yàn)镃Python是大部分環(huán)境下默認(rèn)的Python執(zhí)行環(huán)境。所以在很多人的概念里CPython就是Python,也就想當(dāng)然的把GIL歸結(jié)為Python語言的缺陷。所以這里要先明確一點(diǎn):「GIL并不是Python的特性,Python完全可以不依賴于GIL」
GIL本質(zhì)就是一把互斥鎖
GIL本質(zhì)就是一把互斥鎖,既然是互斥鎖,所有互斥鎖的本質(zhì)都一樣,都是將并發(fā)運(yùn)行變成串行,以此來控制同一時(shí)間內(nèi)共享數(shù)據(jù)只能被一個(gè)任務(wù)所修改,進(jìn)而保證數(shù)據(jù)安全。
可以肯定的一點(diǎn)是:保護(hù)不同的數(shù)據(jù)的安全,就應(yīng)該加不同的鎖。
GIL 的工作原理:比如下面這張圖,就是一個(gè) GIL 在 Python 程序的工作示例。其中,Thread 1、2、3 輪流執(zhí)行,每一個(gè)線程在開始執(zhí)行時(shí),都會(huì)鎖住 GIL,以阻止別的線程執(zhí)行;同樣的,每一個(gè)線程執(zhí)行完一段后,會(huì)釋放 GIL,以允許別的線程開始利用資源。
計(jì)算密集型
計(jì)算密集型任務(wù)的特點(diǎn)是要進(jìn)行大量的計(jì)算,消耗CPU資源
我們先來看一個(gè)簡單的計(jì)算密集型示例:
- '''
- @Author:Runsen
- @微信公眾號(hào):Python之王
- @博客:https://blog.csdn.net/weixin_44510615
- @Date:2020/6/4
- '''
- import time
- COUNT = 50_000_000
- def count_down():
- global COUNT
- while COUNT > 0:
- COUNT -= 1
- s = time.perf_counter()
- count_down()
- c = time.perf_counter() - s
- print('time taken in seconds - >:', c)
- time taken in seconds - >: 9.2957003
這個(gè)是單線程, 時(shí)間是9s, 下面我們用兩個(gè)線程看看結(jié)果又如何:
- '''
- @Author:Runsen
- @微信公眾號(hào):Python之王
- @博客:https://blog.csdn.net/weixin_44510615
- @Date:2020/6/4
- '''
- import time
- from threading import Thread
- COUNT = 50_000_000
- def count_down():
- global COUNT
- while COUNT > 0:
- COUNT -= 1
- s = time.perf_counter()
- t1 = Thread(target=count_down)
- t2 = Thread(target=count_down)
- t1.start()
- t2.start()
- t1.join()
- t2.join()
- c = time.perf_counter() - s
- print('time taken in seconds - >:', c)
- time taken in seconds - >: 17.110625
我們程序主要的操作就是在計(jì)算, CPU沒有等待, 而改為多線程后, 增加了線程后, 在線程之間頻繁的切換,增大了時(shí)間開銷, 時(shí)間當(dāng)然會(huì)增加了。
還有一種類型是IO密集型,涉及到網(wǎng)絡(luò)、磁盤IO的任務(wù)都是IO密集型任務(wù),這類任務(wù)的特點(diǎn)是CPU消耗很少,任務(wù)的大部分時(shí)間都在等待IO操作完成(因?yàn)镮O的速度遠(yuǎn)遠(yuǎn)低于CPU和內(nèi)存的速度)。對于IO密集型任務(wù),任務(wù)越多,CPU效率越高,但也有一個(gè)限度。常見的大部分任務(wù)都是IO密集型任務(wù),比如Web應(yīng)用。
「總結(jié):對于io密集型工作(Python爬蟲),多線程可以大幅提高代碼效率。對CPU計(jì)算密集型(Python數(shù)據(jù)分析,機(jī)器學(xué)習(xí),深度學(xué)習(xí)),多線程的效率可能比單線程還略低。所以,數(shù)據(jù)領(lǐng)域沒有多線程提高效率之說,只有將CPU提升到GPU,TPU來提升計(jì)算能力?!?/strong>