一眯娱、多任務(wù)的引入
在現(xiàn)實生活中,有很多的場景中的事情是同時進行的爬凑,比如開車的時候徙缴,手和腳共同來駕駛汽車;再比如嘁信,唱歌跳舞也是同時進行的于样。
下來,我們在程序里面潘靖,模擬一下“唱歌跳舞”這件事情穿剖,如下:
from time import sleep
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
sleep(1)
if __name__ == '__main__':
sing() #唱歌
dance() #跳舞
注意:
- 很顯然剛剛的程序并沒有完成唱歌和跳舞同時進行的要求。
- 如果想要實現(xiàn)“唱歌跳舞”同時進行卦溢,那么就需要一個新的方法糊余,叫做:多任務(wù)。
二单寂、多任務(wù)的概念
什么叫“多任務(wù)”呢贬芥?簡單地說,就是操作系統(tǒng)可以同時運行多個任務(wù)宣决。打個比方蘸劈,我們一邊在用瀏覽器上網(wǎng),一邊在聽MP3尊沸,一邊在用Word寫作業(yè)威沫,這就是多任務(wù)贤惯,至少同時有3個任務(wù)正在運行。還有很多任務(wù)悄悄地在后臺同時運行著壹甥,只是桌面上沒有顯示而已救巷。
現(xiàn)在,多核CPU已經(jīng)非常普及了句柠。但是浦译,即使過去的單核CPU,也可以執(zhí)行多任務(wù)溯职。由于CPU執(zhí)行代碼都是順序執(zhí)行的精盅,那么,單核CPU是怎么執(zhí)行多任務(wù)的呢谜酒?
答案就是操作系統(tǒng)輪流讓各個任務(wù)交替執(zhí)行叹俏,任務(wù)1執(zhí)行0.01秒,切換到任務(wù)2僻族,任務(wù)2執(zhí)行0.01秒粘驰,再切換到任務(wù)3,執(zhí)行0.01秒……這樣反復(fù)執(zhí)行下去述么。表面上看蝌数,每個任務(wù)都是交替執(zhí)行的,但是度秘,由于CPU的執(zhí)行速度實在是太快了顶伞,我們感覺就像所有任務(wù)都在同時執(zhí)行一樣。
真正的并行執(zhí)行多任務(wù)只能在多核CPU上實現(xiàn)剑梳,但是唆貌,由于任務(wù)數(shù)量遠遠多于CPU的核心數(shù)量,所以垢乙,操作系統(tǒng)也會自動把很多任務(wù)輪流調(diào)度到每個核心上執(zhí)行锨咙。
實現(xiàn)多任務(wù)的策略(調(diào)度算法):
- 時間片輪轉(zhuǎn)
- 優(yōu)先級調(diào)度
實現(xiàn)多任務(wù)的三種方式:
- 進程
- 線程
- 協(xié)程
多任務(wù)的原理:
- 并發(fā):假的多任務(wù),時間片的輪轉(zhuǎn)追逮,快速的交替運行任務(wù)蓖租。
- 并行:真的多任務(wù),一個核處理一個任務(wù)羊壹。
三蓖宦、進程和程序的區(qū)別
編寫完畢的代碼,在沒有運行的時候油猫,稱之為程序稠茂。正在運行著的代碼,就成為進程。
進程睬关,除了包含代碼以外诱担,還有需要運行的環(huán)境等,所以和程序是有區(qū)別的电爹。
四蔫仙、multiprocessing
如果你打算編寫多進程的服務(wù)程序,Unix/Linux無疑是正確的選擇丐箩。由于Windows沒有fork
調(diào)用摇邦,難道在Windows上無法用Python編寫多進程的程序?
由于Python是跨平臺的屎勘,自然也應(yīng)該提供一個跨平臺的多進程支持施籍。multiprocessing
模塊就是跨平臺版本的多進程模塊。
multiprocessing
模塊提供了一個Process
類來代表一個進程對象概漱,下面的例子演示了啟動一個子進程并等待其結(jié)束:
from multiprocessing import Process
import os
# 子進程要執(zhí)行的代碼
def run_proc(name):
print('子進程運行中丑慎,name= %s ,pid=%d...' % (name, os.getpid()))
if __name__ == '__main__':
print('父進程 %d.' % os.getppid())
p = Process(target=run_proc, args=('test',))
print('子進程將要執(zhí)行')
p.start()
p.join()
print('子進程已結(jié)束')
說明:
- 創(chuàng)建子進程時,只需要傳入一個執(zhí)行函數(shù)和函數(shù)的參數(shù)瓤摧,創(chuàng)建一個
Process
實例竿裂,用start()
方法啟動,這樣創(chuàng)建進程比fork()
還要簡單照弥。 -
join()
方法可以等待子進程結(jié)束后再繼續(xù)往下運行腻异,通常用于進程間的同步。
五产喉、Process語法結(jié)構(gòu)
Process([group [, target [, name [, args [, kwargs]]]]])
- target:表示這個進程實例所調(diào)用對象捂掰;
- args:表示調(diào)用對象的位置參數(shù)元組敢会;
- kwargs:表示調(diào)用對象的關(guān)鍵字參數(shù)字典曾沈;
- name:為當(dāng)前進程實例的別名;
- group:大多數(shù)情況下用不到鸥昏;
Process類常用方法:
- is_alive():判斷進程實例是否還在執(zhí)行塞俱;
- join([timeout]):是否等待進程實例執(zhí)行結(jié)束,或等待多少秒吏垮;
- start():啟動進程實例(創(chuàng)建子進程)障涯;
- run():如果沒有給定target參數(shù),對這個對象調(diào)用start()方法時膳汪,就將執(zhí)行對象中的run()方法唯蝶;
- terminate():不管任務(wù)是否完成,立即終止遗嗽;
Process類常用屬性:
- name:當(dāng)前進程實例別名粘我,默認(rèn)為Process-N,N為從1開始遞增的整數(shù);
- pid:當(dāng)前進程實例的PID值征字;
from multiprocessing import Process
import os
from time import sleep
# 子進程要執(zhí)行的代碼
def run_proc(name, age, **kwargs):
for i in range(10):
print('子進程運行中都弹,name= %s,age=%d ,pid=%d...' % (name, age,os.getpid()))
print(kwargs)
sleep(0.5)
if __name__ == '__main__':
print('父進程 %d.' % os.getppid())
p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
print('子進程將要執(zhí)行')
p.start()
sleep(1)
p.terminate()
p.join()
print('子進程已結(jié)束')
進程編號的作用:
- 進程的編號的目的是驗證主進程和子進程的關(guān)系,可以得知子進程是由哪個主進程創(chuàng)建出來的匙姜。
獲取進程編號的方式:
- 獲取主(父)進程pid:
os.getppid()
- 獲取子進程pid:
os.getpid()
- 所有的子進程都來自于父進程畅厢,因此一個程序中的主進程編號得知,子進程編號按照主進程編號為起始值加一計算氮昧。
multiprocessing.current_process()
方法獲取當(dāng)前的當(dāng)前進程的詳細(xì)信息(進程名稱和進程編號)框杜。
multiprocessing.current_process()
:獲取當(dāng)前的當(dāng)前進程的詳細(xì)信息(進程名稱和進程編號)。
from multiprocessing import Process
import time
import os
#兩個子進程將會調(diào)用的兩個方法
def worker_1(interval):
print("worker_1,父進程(%s),當(dāng)前進程(%s)"%(os.getppid(),os.getpid()))
t_start = time.time()
time.sleep(interval) #程序?qū)粧炱餴nterval秒
t_end = time.time()
print("worker_1,執(zhí)行時間為'%0.2f'秒"%(t_end - t_start))
def worker_2(interval):
print("worker_2,父進程(%s),當(dāng)前進程(%s)"%(os.getppid(),os.getpid()))
t_start = time.time()
time.sleep(interval)
t_end = time.time()
print("worker_2,執(zhí)行時間為'%0.2f'秒"%(t_end - t_start))
#輸出當(dāng)前程序的ID
print("進程ID:%s"%os.getpid())
#創(chuàng)建兩個進程對象郭计,target指向這個進程對象要執(zhí)行的對象名稱霸琴,
#args后面的元組中,是要傳遞給worker_1方法的參數(shù)昭伸,
#因為worker_1方法就一個interval參數(shù)梧乘,這里傳遞一個整數(shù)2給它,
#如果不指定name參數(shù)庐杨,默認(rèn)的進程對象名稱為Process-N选调,N為一個遞增的整數(shù)
p1=Process(target=worker_1,args=(2,))
p2=Process(target=worker_2,name="dongGe",args=(1,))
#使用"進程對象名稱.start()"來創(chuàng)建并執(zhí)行一個子進程,
#這兩個進程對象在start后灵份,就會分別去執(zhí)行worker_1和worker_2方法中的內(nèi)容
p1.start()
p2.start()
#同時父進程仍然往下執(zhí)行仁堪,如果p2進程還在執(zhí)行,將會返回True
print("p2.is_alive=%s"%p2.is_alive())
#輸出p1和p2進程的別名和pid
print("p1.name=%s"%p1.name)
print("p1.pid=%s"%p1.pid)
print("p2.name=%s"%p2.name)
print("p2.pid=%s"%p2.pid)
#join括號中不攜帶參數(shù)填渠,表示父進程在這個位置要等待p1進程執(zhí)行完成后弦聂,
#再繼續(xù)執(zhí)行下面的語句,一般用于進程間的數(shù)據(jù)同步氛什,如果不寫這一句莺葫,
#下面的is_alive判斷將會是True,在shell(cmd)里面調(diào)用這個程序時
#可以完整的看到這個過程枪眉,大家可以嘗試著將下面的這條語句改成p1.join(1)捺檬,
#因為p2需要2秒以上才可能執(zhí)行完成,父進程等待1秒很可能不能讓p1完全執(zhí)行完成,
#所以下面的print會輸出True,即p1仍然在執(zhí)行
p1.join()
print("p1.is_alive=%s"%p1.is_alive())
六瞬痘、進程的創(chuàng)建-Process子類
創(chuàng)建新的進程還能夠使用類的方式,可以自定義一個類烤镐,繼承Process
類,每次實例化這個類的時候棍鳖,就等同于實例化一個進程對象炮叶,請看下面的實例:
from multiprocessing import Process
import time
import os
#繼承Process類
class Process_Class(Process):
#因為Process類本身也有__init__方法,這個子類相當(dāng)于重寫了這個方法,
#但這樣就會帶來一個問題悴灵,我們并沒有完全的初始化一個Process類扛芽,所以就不能使用從這個類繼承的一些方法和屬性,
#最好的方法就是將繼承類本身傳遞給Process.__init__方法积瞒,完成這些初始化操作
def __init__(self,interval):
Process.__init__(self)
self.interval = interval
#重寫了Process類的run()方法
def run(self):
print("子進程(%s) 開始執(zhí)行川尖,父進程為(%s)"%(os.getpid(),os.getppid()))
t_start = time.time()
time.sleep(self.interval)
t_stop = time.time()
print("(%s)執(zhí)行結(jié)束,耗時%0.2f秒"%(os.getpid(),t_stop-t_start))
if __name__=="__main__":
t_start = time.time()
print("當(dāng)前程序進程(%s)"%os.getpid())
p1 = Process_Class(2)
#對一個不包含target屬性的Process類執(zhí)行start()方法茫孔,就會運行這個類中的run()方法叮喳,所以這里會執(zhí)行p1.run()
p1.start()
p1.join()
t_stop = time.time()
print("(%s)執(zhí)行結(jié)束,耗時%0.2f"%(os.getpid(),t_stop-t_start))
七缰贝、Process中kill的方法
在python語言中馍悟,我們可以使用os模塊中的kill
方法,根據(jù)pid殺死相應(yīng)進程剩晴。
語法:os.kill(進程編號, 信號編號)
八锣咒、進程的特性
進程的特性介紹:
- 進程之間不共享全局變量
- 主進程會等待所有的子進程結(jié)束之后再結(jié)束
8.1 不共享全局變量
當(dāng)一個進程對全局變量進行數(shù)據(jù)的修改,對于其他進程而言不會造成任何的影響赞弥,可以理解為每個進程都拿的是最初始的全局變量毅整。或者可以理解為全局變量就是所謂資源绽左,當(dāng)創(chuàng)建一個進程悼嫉,則系統(tǒng)會直接給這個進程里面復(fù)制一個全局變量。針對于這個全局變量而言拼窥,在進程之間都是相互獨立存在的戏蔑,之間沒有任何的聯(lián)系。
三個進程分別操作的都是自己進程內(nèi)部的全局變量test_list,不會對其他的進程里面的全局變量造成影響鲁纠,所以進程之間不共享全局變量总棵。他們的關(guān)系只有一點,不同進程之間的全局變量的名字相同而已房交。
8.2 所有子進程結(jié)束主進程才會結(jié)束
主進程會等待所有的子進程執(zhí)行結(jié)束之后才能結(jié)束
在主進程結(jié)束之前彻舰,手動結(jié)束了所有的子進程伐割,那么程序的結(jié)束由主進程的結(jié)束來控制
如果需要實現(xiàn)主進程結(jié)束則整個程序結(jié)束:
- 在主進程結(jié)束之前候味,保證所有子進程結(jié)束使用子進程的terminate()
- 在子進程開啟之前,設(shè)置當(dāng)前子進程被被主進程守護隔心,子進程的deamon屬性設(shè)為true則意味著這個子進程被主進程守護白群,主進程結(jié)束守護結(jié)束,子進程也結(jié)束
九硬霍、單進程與多線程的優(yōu)劣
9.1 單進程
默認(rèn)程序運行創(chuàng)建一個進程帜慢。
一個Python文件運行,就是開啟一個進程去處理。
進程中的場景:主線程去執(zhí)行代碼粱玲。
9.2 多進程
一個Python文件運行躬柬,占用一個進程去處理,假如同時要運行第二個Python文件抽减,同樣給第二個Python文件開啟一個進程去處理允青。
多進程可以完成多任務(wù),每個進程就好比一個獨立車間卵沉,每個車間都各自在運營颠锉,每個進程也是各自在運行,執(zhí)行各自的任務(wù)史汗。
9.3 優(yōu)劣對比
- 單進程開發(fā)簡單琼掠;多線程開發(fā)復(fù)雜;
- 單進程在處理高并發(fā)時一般采用多啟動進程的方式停撞;多線程僅需啟動多個線程瓷蛙。進程的切換開銷比線程大;
- 多進程之間如果有信息通信則相對多線程效率較低戈毒,因為多線程屬于同一地址空間的訪問速挑,效率相對較高(暫不涉及鎖等一致性策略的影響);
- 多進程穩(wěn)定好副硅,一個進程死了不影響其他進程姥宝;多線程中,任意一個出現(xiàn)問題恐疲,將影響到所有腊满。