深度解析Python線程和進(jìn)程
什么是進(jìn)程
進(jìn)程就是操作系統(tǒng)中執(zhí)行的一個程序,操作系統(tǒng)以進(jìn)程為單位分配存儲空間,每個進(jìn)程都有自己的地址空間、數(shù)據(jù)棧以及其他用于跟蹤進(jìn)程執(zhí)行的輔助數(shù)據(jù),操作系統(tǒng)管理所有進(jìn)程的執(zhí)行,為它們合理的分配資源。
每個進(jìn)程都有自己的獨立內(nèi)存空間,不同進(jìn)程通過進(jìn)程間通信來通信。由于進(jìn)程比較重量,占據(jù)獨立的內(nèi)存,所以上下文進(jìn)程間的切換開銷(棧、寄存器、虛擬內(nèi)存、文件句柄等)比較大,但相對比較穩(wěn)定安全。
什么是線程
一個進(jìn)程還可以擁有多個并發(fā)的執(zhí)行線索,簡單的說就是擁有多個可以獲得CPU調(diào)度的執(zhí)行單元,這就是所謂的線程。
CPU調(diào)度和分派的基本單位線程是進(jìn)程的一個實體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨立運行的基本單位。線程自己基本上不擁有系統(tǒng)資源,只擁有一點在運行中必不可少的資源(如程序計數(shù)器,一組寄存器和棧)。
由于線程在同一個進(jìn)程下,它們可以共享相同的上下文,因此相對于進(jìn)程而言,線程間的信息共享和通信更加容易,上下文切換很快,資源開銷較少,但相比進(jìn)程不夠穩(wěn)定容易丟失數(shù)據(jù)。
注:當(dāng)然在單核CPU系統(tǒng)中,真正的并發(fā)是不可能的,因為在某個時刻能夠獲得CPU的只有唯一的一個線程,多個線程共享了CPU的執(zhí)行時間。
線程缺點:
多線程也并不是沒有壞處,站在其他進(jìn)程的角度,多線程的程序?qū)ζ渌绦虿⒉挥押茫驗樗加昧烁嗟腃PU執(zhí)行時間,導(dǎo)致其他程序無法獲得足夠的CPU執(zhí)行時間;另一方面,站在開發(fā)者的角度,編寫和調(diào)試多線程的程序都對開發(fā)者有較高的要求,對于初學(xué)者來說更加困難。
線程與進(jìn)程的區(qū)別
- 地址空間和其他資源:進(jìn)程間相互獨立,同一進(jìn)程的各線程間共享。某線程內(nèi)的想愛你城咋其他進(jìn)程不可見。
- 通信:進(jìn)程間通信IPC,線程間可以直接讀寫進(jìn)程數(shù)據(jù)段來進(jìn)行通信——需要進(jìn)程同步和互斥手段的輔助,以保證數(shù)據(jù)的一致性。
- 調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多。
- 在多線程操作系統(tǒng)中,進(jìn)程不是一個可執(zhí)行的實體
并行與并發(fā)
并行(Parallelism)
并行:指兩個或兩個以上事件(或線程)在同一時刻發(fā)生,是真正意義上的不同事件或線程在同一時刻,在不同CPU資源呢上(多核),同時執(zhí)行。
特點
- 同一時刻發(fā)生,同時執(zhí)行。
- 不存在像并發(fā)那樣競爭,等待的概念。
并發(fā)(Concurrency)
指一個物理CPU(也可以多個物理CPU) 在若干道程序(或線程)之間多路復(fù)用,并發(fā)性是對有限物理資源強制行使多用戶共享以提高效率。
特點
- 微觀角度:所有的并發(fā)處理都有排隊等候,喚醒,執(zhí)行等這樣的步驟,在微觀上他們都是序列被處理的,如果是同一時刻到達(dá)的請求(或線程)也會根據(jù)優(yōu)先級的不同,而先后進(jìn)入隊列排隊等候執(zhí)行。
- 宏觀角度:多個幾乎同時到達(dá)的請求(或線程)在宏觀上看就像是同時在被處理。
Python中的多進(jìn)程
Python中進(jìn)程操作
process模塊是一個創(chuàng)建進(jìn)程的模塊,借助這個模塊,就可以完成進(jìn)程的創(chuàng)建。
法:Process([group [, target [, name [, args [, kwargs]]]]])
由該類實例化得到的對象,表示一個子進(jìn)程中的任務(wù)(尚未啟動)。
注意:
- 必須使用關(guān)鍵字方式來指定參數(shù);
- args指定的為傳給target函數(shù)的位置參數(shù),是一個元祖形式,必須有逗號。
參數(shù)介紹:
- group:參數(shù)未使用,默認(rèn)值為None。
- target:表示調(diào)用對象,即子進(jìn)程要執(zhí)行的任務(wù)。
- args:表示調(diào)用的位置參數(shù)元祖。
- kwargs:表示調(diào)用對象的字典。如kwargs = {'name':Jack, 'age':18}。
- name:子進(jìn)程名稱。
代碼展示:
import os from multiprocessing import Process def func_one(): print("第一個子進(jìn)程") print("子進(jìn)程(一)大兒子:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid())) def func_two(): print("第二個子進(jìn)程") print("子進(jìn)程(二)二兒子:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid())) if __name__ == '__main__': p_one = Process(target=func_one) P_two = Process(target=func_two) p_one.start() P_two.start() print("子進(jìn)程:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid()))
繼承Process的方式開啟進(jìn)程的方式:
import os from multiprocessing import Process def func_one(): print("第一個子進(jìn)程") print("子進(jìn)程(一)大兒子:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid())) def func_two(): print("第二個子進(jìn)程") print("子進(jìn)程(二)二兒子:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid())) if __name__ == '__main__': p_one = Process(target=func_one) P_two = Process(target=func_two) p_one.start() P_two.start() print("子進(jìn)程:%s 父進(jìn)程:%s" % (os.getpid(), os.getppid()))
線程
Python的threading模塊
在Python早期的版本中就引入了thread模塊(現(xiàn)在名為_thread)來實現(xiàn)多線程編程,然而該模塊過于底層,而且很多功能都沒有提供,因此目前的多線程開發(fā)我們推薦使用threading模塊,該模塊對多線程編程提供了更好的面向?qū)ο蟮姆庋b。
from random import randint from threading import Thread from time import time, sleep def download(filename): print('開始下載%s...' % filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下載完成! 耗費了%d秒' % (filename, time_to_download)) def main(): start = time() t1 = Thread(target=download, args=('Python從入門到住院.pdf',)) t1.start() t2 = Thread(target=download, args=('Peking Hot.avi',)) t2.start() #join阻塞完成任務(wù) t1.join() t2.join() end = time() print('總共耗費了%.3f秒' % (end - start)) if __name__ == '__main__': main()
我們可以直接使用threading模塊的Thread
類來創(chuàng)建線程,但是我們之前講過一個非常重要的概念叫“繼承”,我們可以從已有的類創(chuàng)建新類,因此也可以通過繼承Thread
類的方式來創(chuàng)建自定義的線程類,然后再創(chuàng)建線程對象并啟動線程。
代碼如下所示:
from random import randint from threading import Thread from time import time, sleep class DownloadTask(Thread): def __init__(self, filename): super().__init__() self._filename = filename def run(self): print('開始下載%s...' % self._filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下載完成! 耗費了%d秒' % (self._filename, time_to_download)) def main(): start = time() t1 = DownloadTask('Python從入門到住院.pdf') t1.start() t2 = DownloadTask('Peking Hot.avi') t2.start() t1.join() t2.join() end = time() print('總共耗費了%.2f秒.' % (end - start)) if __name__ == '__main__': main()
鎖Lock:
模擬場景:
演示了100個線程向同一個銀行賬戶轉(zhuǎn)賬(轉(zhuǎn)入1元錢)的場景,在這個例子中,銀行賬戶就是一個臨界資源,在沒有保護(hù)的情況下我們很有可能會得到錯誤的結(jié)果。
from time import sleep from threading import Thread class Account(object): #初始化賬戶余額為0元 def __init__(self): self._balance = 0 # 存款函數(shù) def deposit(self, money): # 計算存款后的余額 new_balance = self._balance + money # 模擬受理存款業(yè)務(wù)需要0.01秒的時間 sleep(0.01) # 修改賬戶余額 self._balance = new_balance # set和get @property def balance(self): return self._balance class AddMoneyThread(Thread): def __init__(self, account, money): super().__init__() self._account = account self._money = money def run(self): self._account.deposit(self._money) def main(): # 創(chuàng)建對象 account = Account() threads = [] # 創(chuàng)建100個存款的線程向同一個賬戶中存錢 for _ in range(100): t = AddMoneyThread(account, 1) threads.append(t) t.start() # 等所有存款的線程都執(zhí)行完畢 for t in threads: t.join() print('賬戶余額為: ¥%d元' % account.balance) if __name__ == '__main__': main()
運行結(jié)果:
100個線程分別向賬戶中轉(zhuǎn)入1元錢,結(jié)果居然遠(yuǎn)遠(yuǎn)小于100元。
之所以出現(xiàn)這種情況是因為我們沒有對銀行賬戶這個“臨界資源”加以保護(hù),多個線程同時向賬戶中存錢時,會一起執(zhí)行到new_balance = self._balance + money
這行代碼,多個線程得到的賬戶余額都是初始狀態(tài)下的0
,所以都是0
上面做了+1的操作,因此得到了錯誤的結(jié)果。
在這種情況下,“鎖”就可以派上用場了。我們可以通過“鎖”來保護(hù)“臨界資源”,只有獲得“鎖”的線程才能訪問“臨界資源”,而其他沒有得到“鎖”的線程只能被阻塞起來,直到獲得“鎖”的線程釋放了“鎖”,其他線程才有機會獲得“鎖”,進(jìn)而訪問被保護(hù)的“臨界資源”。
加鎖:
from time import sleep from threading import Thread, Lock class Account(object): def __init__(self): self._balance = 0 self._lock = Lock() def deposit(self, money): # 先獲取鎖才能執(zhí)行后續(xù)的代碼 self._lock.acquire() try: new_balance = self._balance + money sleep(0.01) self._balance = new_balance finally: # 在finally中執(zhí)行釋放鎖的操作保證正常異常鎖都能釋放 self._lock.release() @property def balance(self): return self._balance class AddMoneyThread(Thread): def __init__(self, account, money): super().__init__() self._account = account self._money = money def run(self): # 運行存錢業(yè)務(wù),只有獲取鎖的才能執(zhí)行 self._account.deposit(self._money) def main(): account = Account() threads = [] #創(chuàng)建100個線程 for _ in range(100): # 線程加錢 t = AddMoneyThread(account, 1) threads.append(t) t.start() for t in threads: t.join() print('賬戶余額為: ¥%d元' % account.balance) if __name__ == '__main__': main()
結(jié)果:賬戶余額為: ¥100元
比較遺憾的一件事情是Python的多線程并不能發(fā)揮CPU的多核特性,因為Python的解釋器有一個“全局解釋器鎖”(GIL)的東西,任何線程執(zhí)行前必須先獲得GIL鎖,然后每執(zhí)行100條字節(jié)碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執(zhí)行。
全局解釋器鎖(GIL)
GIL是一個互斥鎖,它防止多個線程同時執(zhí)行Python字節(jié)碼。這個鎖是必要的,主要是因為CPython的內(nèi)存管理不是線程安全的 盡管Python完全支持多線程編程, 但是解釋器的C語言實現(xiàn)部分在完全并行執(zhí)行時并不是線程安全的。
因此,解釋器實際上被一個全局解釋器鎖保護(hù)著,它確保任何時候都只有一個Python線程執(zhí)行。在多線程環(huán)境中,Python 虛擬機按以下方式執(zhí)行:
- 設(shè)置GIL
- 切換到一個線程去執(zhí)行
- 運行
由于GIL的存在,Python的多線程不能稱之為嚴(yán)格的多線程。因為多線程下每個線程在執(zhí)行的過程中都需要先獲取GIL,保證同一時刻只有一個線程在運行。
由于GIL的存在,即使是多線程,事實上同一時刻只能保證一個線程在運行,既然這樣多線程的運行效率不就和單線程一樣了嗎,那為什么還要使用多線程呢?
由于以前的電腦基本都是單核CPU,多線程和單線程幾乎看不出差別,可是由于計算機的迅速發(fā)展,現(xiàn)在的電腦幾乎都是多核CPU了,最少也是兩個核心數(shù)的,這時差別就出來了:通過之前的案例我們已經(jīng)知道,即使在多核CPU中,多線程同一時刻也只有一個線程在運行,這樣不僅不能利用多核CPU的優(yōu)勢,反而由于每個線程在多個CPU上是交替執(zhí)行的,導(dǎo)致在不同CPU上切換時造成資源的浪費,反而會更慢。即原因是一個進(jìn)程只存在一把gil鎖,當(dāng)在執(zhí)行多個線程時,內(nèi)部會爭搶gil鎖,這會造成當(dāng)某一個線程沒有搶到鎖的時候會讓cpu等待,進(jìn)而不能合理利用多核cpu資源。
但是在使用多線程抓取網(wǎng)頁內(nèi)容時,遇到IO阻塞時,正在執(zhí)行的線程會暫時釋放GIL鎖,這時其它線程會利用這個空隙時間,執(zhí)行自己的代碼,因此多線程抓取比單線程抓取性能要好,所以我們還是要使用多線程的。
參考文章:
深度解析Python線程和進(jìn)程 - wyh草樣 - 博客園
Python-100-Days/13.進(jìn)程和線程.md at master · jackfrued/Python-100-Days · GitHub
到此這篇關(guān)于Python深度解析線程和進(jìn)程的文章就介紹到這了,更多相關(guān)python線程和進(jìn)程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python Numpy實現(xiàn)計算矩陣的均值和標(biāo)準(zhǔn)差詳解
NumPy(Numerical Python)是Python的一種開源的數(shù)值計算擴展。這種工具可用來存儲和處理大型矩陣,比Python自身的嵌套列表結(jié)構(gòu)要高效的多。本文主要介紹用NumPy實現(xiàn)計算矩陣的均值和標(biāo)準(zhǔn)差,感興趣的小伙伴可以了解一下2021-11-11關(guān)于pytorch中網(wǎng)絡(luò)loss傳播和參數(shù)更新的理解
今天小編就為大家分享一篇關(guān)于pytorch中網(wǎng)絡(luò)loss傳播和參數(shù)更新的理解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08Python selenium根據(jù)class定位頁面元素的方法
這篇文章主要介紹了Python selenium根據(jù)class定位頁面元素的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-02-02解決python中顯示圖片的plt.imshow plt.show()內(nèi)存泄漏問題
這篇文章主要介紹了解決python中顯示圖片的plt.imshow plt.show()內(nèi)存泄漏問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04