前言
個(gè)人一直覺(jué)得對(duì)學(xué)習(xí)任何知識(shí)而言,概念是相當(dāng)重要的。掌握了概念和原理,細(xì)節(jié)可以留給實(shí)踐去推敲。掌握的關(guān)鍵在于理解,通過(guò)具體的實(shí)例和實(shí)際操作來(lái)感性的體會(huì)概念和原理可以起到很好的效果。本文通過(guò)一些具體的例子簡(jiǎn)單介紹一下python的多線(xiàn)程和多進(jìn)程,后續(xù)會(huì)寫(xiě)一些進(jìn)程通信和線(xiàn)程通信的一些文章。
python多線(xiàn)程
python中提供兩個(gè)標(biāo)準(zhǔn)庫(kù)thread和threading用于對(duì)線(xiàn)程的支持,python3中已放棄對(duì)前者的支持,后者是一種更高層次封裝的線(xiàn)程庫(kù),接下來(lái)均以后者為例。
創(chuàng)建線(xiàn)程
python中有兩種方式實(shí)現(xiàn)線(xiàn)程:
1.實(shí)例化一個(gè)threading.Thread的對(duì)象,并傳入一個(gè)初始化函數(shù)對(duì)象(initial function )作為線(xiàn)程執(zhí)行的入口;
2.繼承threading.Thread,并重寫(xiě)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,并重寫(xiě)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類(lèi)
threading.Thread(group=None, target=None, name=None, args=(), kwargs={})
關(guān)聯(lián)上面兩種創(chuàng)建線(xiàn)程的方式:
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)建線(xiàn)程,指定的參數(shù)最終都會(huì)傳給threading.Thread類(lèi);
2.傳給線(xiàn)程的目標(biāo)函數(shù)是在基類(lèi)Thread的run函數(shù)體中被調(diào)用的,如果run沒(méi)有被重寫(xiě)的話(huà)。
threading模塊的一些屬性和方法可以參照官網(wǎng),這里重點(diǎn)介紹一下threading.Thread對(duì)象的方法
下面是threading.Thread提供的線(xiàn)程對(duì)象方法和屬性:
- start():創(chuàng)建線(xiàn)程后通過(guò)start啟動(dòng)線(xiàn)程,等待CPU調(diào)度,為run函數(shù)執(zhí)行做準(zhǔn)備;
- run():線(xiàn)程開(kāi)始執(zhí)行的入口函數(shù),函數(shù)體中會(huì)調(diào)用用戶(hù)編寫(xiě)的target函數(shù),或者執(zhí)行被重載的run函數(shù);
- join([timeout]):阻塞掛起調(diào)用該函數(shù)的線(xiàn)程,直到被調(diào)用線(xiàn)程執(zhí)行完成或超時(shí)。通常會(huì)在主線(xiàn)程中調(diào)用該方法,等待其他線(xiàn)程執(zhí)行完成。
- name、getName()&setName():線(xiàn)程名稱(chēng)相關(guān)的操作;
- ident:整數(shù)類(lèi)型的線(xiàn)程標(biāo)識(shí)符,線(xiàn)程開(kāi)始執(zhí)行前(調(diào)用start之前)為None;
- isAlive()、is_alive():start函數(shù)執(zhí)行之后到run函數(shù)執(zhí)行完之前都為T(mén)rue;
- daemon、isDaemon()&setDaemon():守護(hù)線(xiàn)程相關(guān);
這些是我們創(chuàng)建線(xiàn)程之后通過(guò)線(xiàn)程對(duì)象對(duì)線(xiàn)程進(jìn)行管理和獲取線(xiàn)程信息的方法。
多線(xiàn)程執(zhí)行
在主線(xiàn)程中創(chuàng)建若線(xiàn)程之后,他們之間沒(méi)有任何協(xié)作和同步,除主線(xiàn)程之外每個(gè)線(xiàn)程都是從run開(kāi)始被執(zhí)行,直到執(zhí)行完畢。
join
我們可以通過(guò)join方法讓主線(xiàn)程阻塞,等待其創(chuàng)建的線(xiàn)程執(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)前線(xiàn)程阻塞,等待t1線(xiàn)程執(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)主線(xiàn)程執(zhí)行完畢之后,當(dāng)前程序并不會(huì)結(jié)束,必須等到所有線(xiàn)程都結(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)建的線(xiàn)程指定為守護(hù)線(xiàn)程(daemon),這樣主線(xiàn)程執(zhí)行完畢之后會(huì)立即結(jié)束未執(zhí)行完的線(xiàn)程,然后結(jié)束程序。
deamon守護(hù)線(xiàn)程
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)前線(xiàn)程阻塞,等待t1線(xiàn)程執(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多線(xiàn)程,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)建線(xiàn)程的方式類(lèi)似:
1.實(shí)例化一個(gè)multiprocessing.Process的對(duì)象,并傳入一個(gè)初始化函數(shù)對(duì)象(initial function )作為新建進(jìn)程執(zhí)行入口;
2.繼承multiprocessing.Process,并重寫(xiě)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())
這里可以思考一下,如果像多線(xiàn)程一樣,存在一個(gè)全局的變量share_data,不同進(jìn)程同時(shí)訪(fǎng)問(wèn)share_data會(huì)有問(wèn)題嗎?
由于每一個(gè)進(jìn)程擁有獨(dú)立的內(nèi)存地址空間且互相隔離,因此不同進(jìn)程看到的share_data是不同的、分別位于不同的地址空間,同時(shí)訪(fǎng)問(wèn)不會(huì)有問(wèn)題。這里需要注意一下。
Subprocess模塊
既然說(shuō)道了多進(jìn)程,那就順便提一下另一種創(chuàng)建進(jìn)程的方式。
python提供了Sunprocess模塊可以在程序執(zhí)行過(guò)程中,調(diào)用外部的程序。
如我們可以在python程序中打開(kāi)記事本,打開(kāi)cmd,或者在某個(gè)時(shí)間點(diǎn)關(guān)機(jī):
>>> import subprocess >>> subprocess.Popen(['cmd'])>>> subprocess.Popen(['notepad']) >>> 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多線(xiàn)程與多進(jìn)程比較
先來(lái)看兩個(gè)例子:
開(kāi)啟兩個(gè)python線(xiàn)程分別做一億次加一操作,和單獨(dú)使用一個(gè)線(xiàn)程做一億次加一操作:
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
上面的例子如果只開(kāi)啟t1和t2兩個(gè)線(xiàn)程中的一個(gè),那么運(yùn)行時(shí)間和主線(xiàn)程基本一致。這個(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)建和銷(xiāo)毀會(huì)進(jìn)行系統(tǒng)調(diào)用,造成額外的時(shí)間開(kāi)銷(xiāo)。
但是對(duì)于python線(xiàn)程,雙線(xiàn)程并行執(zhí)行耗時(shí)比單線(xiàn)程要高的多,效率相差近10倍。如果將兩個(gè)并行線(xiàn)程改成串行執(zhí)行,即:
t1.start() t1.join() t2.start() t2.join() #Two thread cost time: 5.12199997902 #Main thread cost time: 2.54200005531
可以看到三個(gè)線(xiàn)程串行執(zhí)行,每一個(gè)執(zhí)行的時(shí)間基本相同。
本質(zhì)原因雙線(xiàn)程是并發(fā)執(zhí)行的,而不是真正的并行執(zhí)行。原因就在于GIL鎖。
GIL鎖
提起python多線(xiàn)程就不得不提一下GIL(Global Interpreter Lock 全局解釋器鎖),這是目前占統(tǒng)治地位的python解釋器CPython中為了保證數(shù)據(jù)安全所實(shí)現(xiàn)的一種鎖。不管進(jìn)程中有多少線(xiàn)程,只有拿到了GIL鎖的線(xiàn)程才可以在CPU上運(yùn)行,即時(shí)是多核處理器。對(duì)一個(gè)進(jìn)程而言,不管有多少線(xiàn)程,任一時(shí)刻,只會(huì)有一個(gè)線(xiàn)程在執(zhí)行。對(duì)于CPU密集型的線(xiàn)程,其效率不僅僅不高,反而有可能比較低。python多線(xiàn)程比較適用于IO密集型的程序。對(duì)于的確需要并行運(yùn)行的程序,可以考慮多進(jìn)程。
多線(xiàn)程對(duì)鎖的爭(zhēng)奪,CPU對(duì)線(xiàn)程的調(diào)度,線(xiàn)程之間的切換等均會(huì)有時(shí)間開(kāi)銷(xiāo)。
線(xiàn)程與進(jìn)程區(qū)別
下面簡(jiǎn)單的比較一下線(xiàn)程與進(jìn)程
- 進(jìn)程是資源分配的基本單位,線(xiàn)程是CPU執(zhí)行和調(diào)度的基本單位;
-
通信/同步方式:
-
進(jìn)程:
- 通信方式:管道,F(xiàn)IFO,消息隊(duì)列,信號(hào),共享內(nèi)存,socket,stream流;
- 同步方式:PV信號(hào)量,管程
-
線(xiàn)程:
- 同步方式:互斥鎖,遞歸鎖,條件變量,信號(hào)量
- 通信方式:位于同一進(jìn)程的線(xiàn)程共享進(jìn)程資源,因此線(xiàn)程間沒(méi)有類(lèi)似于進(jìn)程間用于數(shù)據(jù)傳遞的通信方式,線(xiàn)程間的通信主要是用于線(xiàn)程同步。
-
進(jìn)程:
- CPU上真正執(zhí)行的是線(xiàn)程,線(xiàn)程比進(jìn)程輕量,其切換和調(diào)度代價(jià)比進(jìn)程要小;
- 線(xiàn)程間對(duì)于共享的進(jìn)程數(shù)據(jù)需要考慮線(xiàn)程安全問(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ù)段、堆空間和棧空間,以及操作系統(tǒng)共享部分 ,有等待,就緒和運(yùn)行三種狀態(tài);
- 一個(gè)進(jìn)程可以包含多個(gè)線(xiàn)程,線(xiàn)程之間共享進(jìn)程的資源(文件描述符、全局變量、堆空間等),寄存器變量和棧空間等是線(xiàn)程私有的;
- 操作系統(tǒng)中一個(gè)進(jìn)程掛掉不會(huì)影響其他進(jìn)程,如果一個(gè)進(jìn)程中的某個(gè)線(xiàn)程掛掉而且OS對(duì)線(xiàn)程的支持是多對(duì)一模型,那么會(huì)導(dǎo)致當(dāng)前進(jìn)程掛掉;
- 如果CPU和系統(tǒng)支持多線(xiàn)程與多進(jìn)程,多個(gè)進(jìn)程并行執(zhí)行的同時(shí),每個(gè)進(jìn)程中的線(xiàn)程也可以并行執(zhí)行,這樣才能最大限度的榨取硬件的性能;
線(xiàn)程和進(jìn)程的上下文切換
進(jìn)程切換過(guò)程切換牽涉到非常多的東西,寄存器內(nèi)容保存到任務(wù)狀態(tài)段TSS,切換頁(yè)表,堆棧等。簡(jiǎn)單來(lái)說(shuō)可以分為下面兩步:
頁(yè)全局目錄切換,使CPU到新進(jìn)程的線(xiàn)性地址空間尋址;
切換內(nèi)核態(tài)堆棧和硬件上下文,硬件上下文包含CPU寄存器的內(nèi)容,存放在TSS中;
線(xiàn)程運(yùn)行于進(jìn)程地址空間,切換過(guò)程不涉及到空間的變換,只牽涉到第二步;
使用多線(xiàn)程還是多進(jìn)程?
CPU密集型:程序需要占用CPU進(jìn)行大量的運(yùn)算和數(shù)據(jù)處理;
I/O密集型:程序中需要頻繁的進(jìn)行I/O操作;例如網(wǎng)絡(luò)中socket數(shù)據(jù)傳輸和讀取等;
由于python多線(xiàn)程并不是并行執(zhí)行,因此較適合與I/O密集型程序,多進(jìn)程并行執(zhí)行適用于CPU密集型程序;
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

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