python多線程與多進(jìn)程及其區(qū)別詳解
前言
個(gè)人一直覺得對(duì)學(xué)習(xí)任何知識(shí)而言,概念是相當(dāng)重要的。掌握了概念和原理,細(xì)節(jié)可以留給實(shí)踐去推敲。掌握的關(guān)鍵在于理解,通過(guò)具體的實(shí)例和實(shí)際操作來(lái)感性的體會(huì)概念和原理可以起到很好的效果。本文通過(guò)一些具體的例子簡(jiǎn)單介紹一下python的多線程和多進(jìn)程,后續(xù)會(huì)寫一些進(jìn)程通信和線程通信的一些文章。
python多線程
python中提供兩個(gè)標(biāo)準(zhǔn)庫(kù)thread和threading用于對(duì)線程的支持,python3中已放棄對(duì)前者的支持,后者是一種更高層次封裝的線程庫(kù),接下來(lái)均以后者為例。
創(chuàng)建線程
python中有兩種方式實(shí)現(xiàn)線程:
1.實(shí)例化一個(gè)threading.Thread的對(duì)象,并傳入一個(gè)初始化函數(shù)對(duì)象(initial function )作為線程執(zhí)行的入口;
2.繼承threading.Thread,并重寫run函數(shù);
方式1:創(chuàng)建threading.Thread對(duì)象
import threading import time def tstart(arg): time.sleep(0.5) print("%s running...." % arg) if __name__ == '__main__': t1 = threading.Thread(target=tstart, args=('This is thread 1',)) t2 = threading.Thread(target=tstart, args=('This is thread 2',)) t1.start() t2.start() print("This is main function")
結(jié)果:
This is main function This is thread 2 running.... This is thread 1 running....
方式2:繼承threading.Thread,并重寫run
import threading import time class CustomThread(threading.Thread): def __init__(self, thread_name): # step 1: call base __init__ function super(CustomThread, self).__init__(name=thread_name) self._tname = thread_name def run(self): # step 2: overide run function time.sleep(0.5) print("This is %s running...." % self._tname) if __name__ == "__main__": t1 = CustomThread("thread 1") t2 = CustomThread("thread 2") t1.start() t2.start() print("This is main function")
執(zhí)行結(jié)果同方式1.
threading.Thread
上面兩種方法本質(zhì)上都是直接或者間接使用threading.Thread類
threading.Thread(group=None, target=None, name=None, args=(), kwargs={})
關(guān)聯(lián)上面兩種創(chuàng)建線程的方式:
import threading import time class CustomThread(threading.Thread): def __init__(self, thread_name, target = None): # step 1: call base __init__ function super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,)) self._tname = thread_name def run(self): # step 2: overide run function # time.sleep(0.5) # print("This is %s running....@run" % self._tname) super(CustomThread, self).run() def target(arg): time.sleep(0.5) print("This is %s running....@target" % arg) if __name__ == "__main__": t1 = CustomThread("thread 1", target) t2 = CustomThread("thread 2", target) t1.start() t2.start() print("This is main function")
結(jié)果:
This is main function This is thread 1 running....@target This is thread 2 running....@target
上面這段代碼說(shuō)明:
1.兩種方式創(chuàng)建線程,指定的參數(shù)最終都會(huì)傳給threading.Thread類;
2.傳給線程的目標(biāo)函數(shù)是在基類Thread的run函數(shù)體中被調(diào)用的,如果run沒(méi)有被重寫的話。
threading模塊的一些屬性和方法可以參照官網(wǎng),這里重點(diǎn)介紹一下threading.Thread對(duì)象的方法
下面是threading.Thread提供的線程對(duì)象方法和屬性:
- start():創(chuàng)建線程后通過(guò)start啟動(dòng)線程,等待CPU調(diào)度,為run函數(shù)執(zhí)行做準(zhǔn)備;
- run():線程開始執(zhí)行的入口函數(shù),函數(shù)體中會(huì)調(diào)用用戶編寫的target函數(shù),或者執(zhí)行被重載的run函數(shù);
- join([timeout]):阻塞掛起調(diào)用該函數(shù)的線程,直到被調(diào)用線程執(zhí)行完成或超時(shí)。通常會(huì)在主線程中調(diào)用該方法,等待其他線程執(zhí)行完成。
- name、getName()&setName():線程名稱相關(guān)的操作;
- ident:整數(shù)類型的線程標(biāo)識(shí)符,線程開始執(zhí)行前(調(diào)用start之前)為None;
- isAlive()、is_alive():start函數(shù)執(zhí)行之后到run函數(shù)執(zhí)行完之前都為True;
- daemon、isDaemon()&setDaemon():守護(hù)線程相關(guān);
這些是我們創(chuàng)建線程之后通過(guò)線程對(duì)象對(duì)線程進(jìn)行管理和獲取線程信息的方法。
多線程執(zhí)行
在主線程中創(chuàng)建若線程之后,他們之間沒(méi)有任何協(xié)作和同步,除主線程之外每個(gè)線程都是從run開始被執(zhí)行,直到執(zhí)行完畢。
join
我們可以通過(guò)join方法讓主線程阻塞,等待其創(chuàng)建的線程執(zhí)行完成。
import threading import time def tstart(arg): print("%s running....at: %s" % (arg,time.time())) time.sleep(1) print("%s is finished! at: %s" % (arg,time.time())) if __name__ == '__main__': t1 = threading.Thread(target=tstart, args=('This is thread 1',)) t1.start() t1.join() # 當(dāng)前線程阻塞,等待t1線程執(zhí)行完成 print("This is main function at:%s" % time.time())
結(jié)果:
This is thread 1 running....at: 1564906617.43 This is thread 1 is finished! at: 1564906618.43 This is main function at:1564906618.43
如果不加任何限制,當(dāng)主線程執(zhí)行完畢之后,當(dāng)前程序并不會(huì)結(jié)束,必須等到所有線程都結(jié)束之后才能結(jié)束當(dāng)前進(jìn)程。
將上面程序中的t1.join()去掉,執(zhí)行結(jié)果如下:
This is thread 1 running....at: 1564906769.52 This is main function at:1564906769.52 This is thread 1 is finished! at: 1564906770.52
可以通過(guò)將創(chuàng)建的線程指定為守護(hù)線程(daemon),這樣主線程執(zhí)行完畢之后會(huì)立即結(jié)束未執(zhí)行完的線程,然后結(jié)束程序。
deamon守護(hù)線程
import threading import time def tstart(arg): print("%s running....at: %s" % (arg,time.time())) time.sleep(1) print("%s is finished! at: %s" % (arg,time.time())) if __name__ == '__main__': t1 = threading.Thread(target=tstart, args=('This is thread 1',)) t1.setDaemon(True) t1.start() # t1.join() # 當(dāng)前線程阻塞,等待t1線程執(zhí)行完成 print("This is main function at:%s" % time.time())
結(jié)果:
This is thread 1 running....at: 1564906847.85 This is main function at:1564906847.85
python多進(jìn)程
相比較于threading模塊用于創(chuàng)建python多線程,python提供multiprocessing用于創(chuàng)建多進(jìn)程。先看一下創(chuàng)建進(jìn)程的兩種方式。
The multiprocessing package mostly replicates the API of the threading module. —— python doc
創(chuàng)建進(jìn)程
創(chuàng)建進(jìn)程的方式和創(chuàng)建線程的方式類似:
1.實(shí)例化一個(gè)multiprocessing.Process的對(duì)象,并傳入一個(gè)初始化函數(shù)對(duì)象(initial function )作為新建進(jìn)程執(zhí)行入口;
2.繼承multiprocessing.Process,并重寫run函數(shù);
方式1:
from multiprocessing import Process import os, time def pstart(name): # time.sleep(0.1) print("Process name: %s, pid: %s "%(name, os.getpid())) if __name__ == "__main__": subproc = Process(target=pstart, args=('subprocess',)) subproc.start() subproc.join() print("subprocess pid: %s"%subproc.pid) print("current process pid: %s" % os.getpid())
結(jié)果:
Process name: subprocess, pid: 4888 subprocess pid: 4888 current process pid: 9912
方式2:
from multiprocessing import Process import os, time class CustomProcess(Process): def __init__(self, p_name, target=None): # step 1: call base __init__ function() super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,)) def run(self): # step 2: # time.sleep(0.1) print("Custom Process name: %s, pid: %s "%(self.name, os.getpid())) if __name__ == '__main__': p1 = CustomProcess("process_1") p1.start() p1.join() print("subprocess pid: %s"%p1.pid) print("current process pid: %s" % os.getpid())
這里可以思考一下,如果像多線程一樣,存在一個(gè)全局的變量share_data,不同進(jìn)程同時(shí)訪問(wèn)share_data會(huì)有問(wèn)題嗎?
由于每一個(gè)進(jìn)程擁有獨(dú)立的內(nèi)存地址空間且互相隔離,因此不同進(jìn)程看到的share_data是不同的、分別位于不同的地址空間,同時(shí)訪問(wèn)不會(huì)有問(wèn)題。這里需要注意一下。
Subprocess模塊
既然說(shuō)道了多進(jìn)程,那就順便提一下另一種創(chuàng)建進(jìn)程的方式。
python提供了Sunprocess模塊可以在程序執(zhí)行過(guò)程中,調(diào)用外部的程序。
如我們可以在python程序中打開記事本,打開cmd,或者在某個(gè)時(shí)間點(diǎn)關(guān)機(jī):
>>> import subprocess >>> subprocess.Popen(['cmd']) <subprocess.Popen object at 0x0339F550> >>> subprocess.Popen(['notepad']) <subprocess.Popen object at 0x03262B70> >>> subprocess.Popen(['shutdown', '-p'])
或者使用ping測(cè)試一下網(wǎng)絡(luò)連通性:
>>> res = subprocess.Popen(['ping', 'www.cnblogs.com'], stdout=subprocess.PIPE).communicate()[0] >>> print res 正在 Ping www.cnblogs.com [101.37.113.127] 具有 32 字節(jié)的數(shù)據(jù): 來(lái)自 101.37.113.127 的回復(fù): 字節(jié)=32 時(shí)間=1ms TTL=91 來(lái)自 101.37.113.127 的回復(fù): 字節(jié)=32 時(shí)間=1ms TTL=91 來(lái)自 101.37.113.127 的回復(fù): 字節(jié)=32 時(shí)間=1ms TTL=91 來(lái)自 101.37.113.127 的回復(fù): 字節(jié)=32 時(shí)間=1ms TTL=91 101.37.113.127 的 Ping 統(tǒng)計(jì)信息: 數(shù)據(jù)包: 已發(fā)送 = 4,已接收 = 4,丟失 = 0 (0% 丟失), 往返行程的估計(jì)時(shí)間(以毫秒為單位): 最短 = 1ms,最長(zhǎng) = 1ms,平均 = 1ms
python多線程與多進(jìn)程比較
先來(lái)看兩個(gè)例子:
開啟兩個(gè)python線程分別做一億次加一操作,和單獨(dú)使用一個(gè)線程做一億次加一操作:
def tstart(arg): var = 0 for i in xrange(100000000): var += 1 if __name__ == '__main__': t1 = threading.Thread(target=tstart, args=('This is thread 1',)) t2 = threading.Thread(target=tstart, args=('This is thread 2',)) start_time = time.time() t1.start() t2.start() t1.join() t2.join() print("Two thread cost time: %s" % (time.time() - start_time)) start_time = time.time() tstart("This is thread 0") print("Main thread cost time: %s" % (time.time() - start_time))
結(jié)果:
Two thread cost time: 20.6570000648 Main thread cost time: 2.52800011635
上面的例子如果只開啟t1和t2兩個(gè)線程中的一個(gè),那么運(yùn)行時(shí)間和主線程基本一致。這個(gè)后面會(huì)解釋原因。
使用兩個(gè)進(jìn)程進(jìn)行上面的操作:
def pstart(arg): var = 0 for i in xrange(100000000): var += 1 if __name__ == '__main__': p1 = Process(target = pstart, args = ("1", )) p2 = Process(target = pstart, args = ("2", )) start_time = time.time() p1.start() p2.start() p1.join() p2.join() print("Two process cost time: %s" % (time.time() - start_time)) start_time = time.time() pstart("0") print("Current process cost time: %s" % (time.time() - start_time))
結(jié)果:
Two process cost time: 2.91599988937 Current process cost time: 2.52400016785
對(duì)比分析
雙進(jìn)程并行執(zhí)行和單進(jìn)程執(zhí)行相同的運(yùn)算代碼,耗時(shí)基本相同,雙進(jìn)程耗時(shí)會(huì)稍微多一些,可能的原因是進(jìn)程創(chuàng)建和銷毀會(huì)進(jìn)行系統(tǒng)調(diào)用,造成額外的時(shí)間開銷。
但是對(duì)于python線程,雙線程并行執(zhí)行耗時(shí)比單線程要高的多,效率相差近10倍。如果將兩個(gè)并行線程改成串行執(zhí)行,即:
t1.start() t1.join() t2.start() t2.join() #Two thread cost time: 5.12199997902 #Main thread cost time: 2.54200005531
可以看到三個(gè)線程串行執(zhí)行,每一個(gè)執(zhí)行的時(shí)間基本相同。
本質(zhì)原因雙線程是并發(fā)執(zhí)行的,而不是真正的并行執(zhí)行。原因就在于GIL鎖。
GIL鎖
提起python多線程就不得不提一下GIL(Global Interpreter Lock 全局解釋器鎖),這是目前占統(tǒng)治地位的python解釋器CPython中為了保證數(shù)據(jù)安全所實(shí)現(xiàn)的一種鎖。不管進(jìn)程中有多少線程,只有拿到了GIL鎖的線程才可以在CPU上運(yùn)行,即時(shí)是多核處理器。對(duì)一個(gè)進(jìn)程而言,不管有多少線程,任一時(shí)刻,只會(huì)有一個(gè)線程在執(zhí)行。對(duì)于CPU密集型的線程,其效率不僅僅不高,反而有可能比較低。python多線程比較適用于IO密集型的程序。對(duì)于的確需要并行運(yùn)行的程序,可以考慮多進(jìn)程。
多線程對(duì)鎖的爭(zhēng)奪,CPU對(duì)線程的調(diào)度,線程之間的切換等均會(huì)有時(shí)間開銷。
線程與進(jìn)程區(qū)別
下面簡(jiǎn)單的比較一下線程與進(jìn)程
- 進(jìn)程是資源分配的基本單位,線程是CPU執(zhí)行和調(diào)度的基本單位;
- 通信/同步方式:
- 進(jìn)程:
- 通信方式:管道,F(xiàn)IFO,消息隊(duì)列,信號(hào),共享內(nèi)存,socket,stream流;
- 同步方式:PV信號(hào)量,管程
- 線程:
- 同步方式:互斥鎖,遞歸鎖,條件變量,信號(hào)量
- 通信方式:位于同一進(jìn)程的線程共享進(jìn)程資源,因此線程間沒(méi)有類似于進(jìn)程間用于數(shù)據(jù)傳遞的通信方式,線程間的通信主要是用于線程同步。
- 進(jìn)程:
- CPU上真正執(zhí)行的是線程,線程比進(jìn)程輕量,其切換和調(diào)度代價(jià)比進(jìn)程要??;
- 線程間對(duì)于共享的進(jìn)程數(shù)據(jù)需要考慮線程安全問(wèn)題,由于進(jìn)程之間是隔離的,擁有獨(dú)立的內(nèi)存空間資源,相對(duì)比較安全,只能通過(guò)上面列出的IPC(Inter-Process Communication)進(jìn)行數(shù)據(jù)傳輸;
- 系統(tǒng)有一個(gè)個(gè)進(jìn)程組成,每個(gè)進(jìn)程包含代碼段、數(shù)據(jù)段、堆空間和??臻g,以及操作系統(tǒng)共享部分 ,有等待,就緒和運(yùn)行三種狀態(tài);
- 一個(gè)進(jìn)程可以包含多個(gè)線程,線程之間共享進(jìn)程的資源(文件描述符、全局變量、堆空間等),寄存器變量和??臻g等是線程私有的;
- 操作系統(tǒng)中一個(gè)進(jìn)程掛掉不會(huì)影響其他進(jìn)程,如果一個(gè)進(jìn)程中的某個(gè)線程掛掉而且OS對(duì)線程的支持是多對(duì)一模型,那么會(huì)導(dǎo)致當(dāng)前進(jìn)程掛掉;
- 如果CPU和系統(tǒng)支持多線程與多進(jìn)程,多個(gè)進(jìn)程并行執(zhí)行的同時(shí),每個(gè)進(jìn)程中的線程也可以并行執(zhí)行,這樣才能最大限度的榨取硬件的性能;
線程和進(jìn)程的上下文切換
進(jìn)程切換過(guò)程切換牽涉到非常多的東西,寄存器內(nèi)容保存到任務(wù)狀態(tài)段TSS,切換頁(yè)表,堆棧等。簡(jiǎn)單來(lái)說(shuō)可以分為下面兩步:
頁(yè)全局目錄切換,使CPU到新進(jìn)程的線性地址空間尋址;
切換內(nèi)核態(tài)堆棧和硬件上下文,硬件上下文包含CPU寄存器的內(nèi)容,存放在TSS中;
線程運(yùn)行于進(jìn)程地址空間,切換過(guò)程不涉及到空間的變換,只牽涉到第二步;
使用多線程還是多進(jìn)程?
CPU密集型:程序需要占用CPU進(jìn)行大量的運(yùn)算和數(shù)據(jù)處理;
I/O密集型:程序中需要頻繁的進(jìn)行I/O操作;例如網(wǎng)絡(luò)中socket數(shù)據(jù)傳輸和讀取等;
由于python多線程并不是并行執(zhí)行,因此較適合與I/O密集型程序,多進(jìn)程并行執(zhí)行適用于CPU密集型程序;
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python+OpenCV 圖像邊緣檢測(cè)四種實(shí)現(xiàn)方法
本文主要介紹了通過(guò)OpenCV中Sobel算子、Schaar算子、Laplacian算子以及Canny分別實(shí)現(xiàn)圖像邊緣檢測(cè)并總結(jié)了四者的優(yōu)缺點(diǎn),感興趣的同學(xué)可以參考一下2021-11-11Python操作PDF實(shí)現(xiàn)制作數(shù)據(jù)報(bào)告
Python操作PDF的庫(kù)有很多,比如PyPDF2、pdfplumber、PyMuPDF等等。本文將利用FPDF模塊操作PDF實(shí)現(xiàn)制作數(shù)據(jù)報(bào)告,感興趣的小伙伴可以嘗試一下2022-12-12解決Python計(jì)算矩陣乘向量,矩陣乘實(shí)數(shù)的一些小錯(cuò)誤
今天小編就為大家分享一篇解決Python計(jì)算矩陣乘向量,矩陣乘實(shí)數(shù)的一些小錯(cuò)誤,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08基于Python實(shí)現(xiàn)開發(fā)釘釘通知機(jī)器人
在項(xiàng)目協(xié)同工作或自動(dòng)化流程完成時(shí),我們需要用一定的手段通知自己或他人。Telegram 非常好用,幾個(gè)步驟就能創(chuàng)建一個(gè)機(jī)器人,可惜在國(guó)內(nèi)無(wú)法使用。所以本文就來(lái)開發(fā)一個(gè)釘釘通知機(jī)器人吧2023-02-02Python采集天天基金數(shù)據(jù)掌握最新基金動(dòng)向
這篇文章主要介紹了Python采集天天基金數(shù)據(jù)掌握最新基金動(dòng)向,本次案例實(shí)現(xiàn)流程為發(fā)送請(qǐng)求、獲取數(shù)據(jù)、解析數(shù)據(jù)、多頁(yè)爬取、保存數(shù)據(jù),接下來(lái)來(lái)看看具體的操作過(guò)程吧2022-01-01公眾號(hào)接入chatGPT的詳細(xì)教程 附Python源碼
這篇文章主要介紹了公眾號(hào)接入chatGPT教程附Python源碼,這里需要大家準(zhǔn)備一個(gè)域名,一臺(tái)服務(wù)器和一個(gè)公眾號(hào),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-02-02