要讓Python程序?qū)崿F(xiàn)多進程(multiprocessing),我們先了解操作系統(tǒng)的相關(guān)知識。Unix/Linux操作系統(tǒng)提供了一個fork()系統(tǒng)調(diào)用,它非常特殊。普通的函數(shù)調(diào)用,調(diào)用一次,返回一次,但是fork()調(diào)用一次,返回兩次,因為操作系統(tǒng)自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內(nèi)返回。
子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調(diào)用getppid()就可以拿到父進程的ID。Python的os模塊封裝了常見的系統(tǒng)調(diào)用,其中就包括fork。可以在linux系統(tǒng)下的Python程序中輕松創(chuàng)建子進程:
import os
import time
pid = os.fork()
if pid == 0:
print('哈哈。。。')
else:
print('嘿嘿。。。')
print('中午了。。。')
結(jié)果如下:
可以利用多進程調(diào)用函數(shù):
import os
import time
def sing():
for i in range(3):
print('唱歌。。。')
print(time.ctime())
time.sleep(1)
def dance():
for i in range(3):
print('跳舞。。。')
print(time.ctime())
time.sleep(1)
if __name__ == '__main__':
pid = os.fork()
if pid == 0:
sing()
else:
dance()
結(jié)果如下:
在前面提到了進程號,什么是進程號(PID)呢?打開我們的資源管理器:
可以使用os模塊里的getpid()得到自己的進程號,使用getppid()得到父進程的進程號。
import os
pid = os.fork()
print(pid)
if pid == 0:
print('哈哈。。。')
print('我是子進程:%s,我的父進程:%s'%(os.getpid(),os.getppid()))
else:
print('嘿嘿。。。')
print('我是父進程:%s,我的父進程:%s'%(os.getpid(),os.getppid()))
結(jié)果如下:
可以清楚的看到,父進程返回的值是子進程的進程號。
當有一個全局變量時,在父進程或子進程中改變?nèi)肿兞浚瑫ζ渌M程有影響嗎?
import os
import time
num = 10
pid = os.fork()
print(pid)
if pid == 0:
num = num +10
print('哈哈。。。num=%s'%num)
print('我是子進程:%s,我的父進程:%s'%(os.getpid(),os.getppid()))
else:
time.sleep(3)
num = num*10
print('嘿嘿。。。num=%s'%num)
print('我是父進程:%s,我的父進程:%s'%(os.getpid(),os.getppid()))
結(jié)果如下:
正如之前所說,當執(zhí)行fork()語句時,子進程會將父進程的代碼拷貝一份,各執(zhí)行各的,互不影響。
那么多個fork是如何執(zhí)行的?
import os
import time
pid = os.fork()
if pid == 0:
print('哈哈1。。。')
else:
print('主進程嘿嘿1。。。')
pid = os.fork()
if pid == 0:
print('哈哈2。。。')
else:
print('主進程嘿嘿2。。。')
結(jié)果如下:
如果你打算編寫多進程的服務程序,Unix/Linux無疑是正確的選擇。由于Windows沒有fork調(diào)用,難道在Windows上無法用Python編寫多進程的程序?由于Python是跨平臺的,自然也應該提供一個跨平臺的多進程支持。multiprocessing模塊就是跨平臺版本的多進程模塊。
import multiprocessing
import os
def foo():
print('子進程id:%s父進程id:%s' % (os.getpid(), os.getppid()))
if __name__ == '__main__':
print('父進程id:%s' % os.getpid())
p = multiprocessing.Process(target=foo)
print('子進程將要執(zhí)行。。。')
p.start()
print('主進程結(jié)束。。')
結(jié)果如下:
對比linux下fork()的多進程,出現(xiàn)了明顯的區(qū)別,在第一個例子中,父進程和子進程都打印了‘中午了’。而在multiprocessing的模塊下,子進程只進行自己函數(shù)里的內(nèi)容,并沒有打印‘主程序結(jié)束這句話’。只有主程序打印了這句話。
Process語法結(jié)構(gòu)如下:
target:表示這個進程實例所調(diào)用對象;
args:表示調(diào)用對象的位置參數(shù)元組;
kwargs:表示調(diào)用對象的關(guān)鍵字參數(shù)字典;
name:為當前進程實例的別名;
group:大多數(shù)情況下用不到;
Process類常用方法;
is_alive():判斷進程實例是否還在執(zhí)行;
join([timeout]):是否等待進程實例執(zhí)行結(jié)束,或等待多少秒;
start():啟動進程實例(創(chuàng)建子進程);
run():如果沒有給定target參數(shù),對這個對象調(diào)用start()方法時,就將執(zhí)行對象中的run()方法;
terminate():不管任務是否完成,立即終止;
Process類常用屬性;
name:當前進程實例別名,默認為Process-N,N為從1開始遞增的整數(shù);
pid:當前進程實例的PID值;
創(chuàng)建子進程時,只需要傳入一個執(zhí)行函數(shù)和函數(shù)的參數(shù),創(chuàng)建一個Process實例,用start()方法啟動,這樣創(chuàng)建進程比fork()還要簡單。join()方法可以等待子進程結(jié)束后再繼續(xù)往下運行,通常用于進程間的同步。代碼如下:
import multiprocessing
import os
def foo():
print('子進程id:%s父進程id:%s' % (os.getpid(), os.getppid()))
if __name__ == '__main__':
print('父進程id:%s' % os.getpid())
p = multiprocessing.Process(target=foo)
print('子進程將要執(zhí)行。。。')
p.start()
p.join()
print('主進程結(jié)束。。')
結(jié)果如下:
傳遞其他的參數(shù)代碼如下:
import multiprocessing
import os
def foo(name, no, **kwargs):
print('%s子進程%s的id:%s父進程id:%s' % (no, name, os.getpid(), os.getppid()))
print(kwargs)
if __name__ == '__main__':
print('父進程id:%s' % os.getpid())
p1 = multiprocessing.Process(target=foo, name='哈哈', args=('xx', 18), kwargs={'1': 'haha', '2': 'hehe'})
p2 = multiprocessing.Process(target=foo, args=('ff', 30), kwargs={'xx': '哈哈', 'kk': '嘿嘿'})
print('子進程將要執(zhí)行。。。')
p1.start()
print(p1.name)
p2.start()
print(p2.name)
p1.join()
p2.join()
print('主進程結(jié)束。。')
結(jié)果如下:
可以看出如果設置name,打印進程的名字是按設置的name來,如果不設置,系統(tǒng)默認的是Process-x。
有了以上的基礎,就可以試著模擬多進程的下載過程。
import multiprocessing
import time
import random
def download(name):
print('文件%s添加至下載隊列。。。' % name)
time.sleep(random.randint(2, 5))
print('文件%s下載成功。。。' % name)
if __name__ == '__main__':
p1 = multiprocessing.Process(target=download, name='哈哈', args=('三國_01.avi',))
p2 = multiprocessing.Process(target=download, args=('三國_02.avi',))
p3 = multiprocessing.Process(target=download, args=('三國_03.avi',))
print('將要開始下載。。。')
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print('下載結(jié)束。。')
結(jié)果如下:
創(chuàng)建進程Process的子類。也就是說創(chuàng)建新的進程還能夠使用類的方法,可以自定義一個類,繼承Process類,每次實例化這個類的時候,就等同于實例化一個進程對象。
import multiprocessing
import os
class MyProcess(multiprocessing.Process):
def __init__(self, name):
super().__init__(name=name)
def run(self):
print('我是子進程:%s,我的PID是%s' % (self.name, os.getpid()))
print('我的父進程PID是:%s' % os.getppid())
if __name__ == '__main__':
print('程序開始。。。')
print('程序PID是:%s' % os.getpid())
p1 = MyProcess('進程一')
p1.start()
p1.join()
print('程序結(jié)束。。。')
輸出結(jié)果:
因為Process類本身也有init方法,這個子類相當于重寫了這個方法。所以再次調(diào)用父類的方法,完成初始化操作。
當需要創(chuàng)建的子進程數(shù)量不多時,可以直接利用multiprocessing中的Process動態(tài)成生多個進程,但如果是上百甚至上千個目標,手動的去創(chuàng)建進程的工作量巨大,此時就可以用到multiprocessing模塊提供的Pool方法。初始化Pool時,可以指定一個最大進程數(shù),當有新的請求提交到Pool中時,如果池還沒有滿,那么就會創(chuàng)建一個新的進程用來執(zhí)行該請求;但如果池中的進程數(shù)已經(jīng)達到指定的最大值,那么該請求就會等待,直到池中有進程結(jié)束,才會創(chuàng)建新的進程來執(zhí)行。
import multiprocessing
import os, time, random
def worker(msg):
t_start = time.time()
print("%s開始執(zhí)行,進程號為%d" % (msg, os.getpid()))
# random.random()隨機生成0~1之間的浮點數(shù)
time.sleep(random.random() * 2)
t_stop = time.time()
print(msg, "執(zhí)行完畢,耗時%0.2f" % (t_stop - t_start))
if __name__ == '__main__':
po = multiprocessing.Pool(3) # 定義一個進程池,最大進程數(shù)3
for i in range(0, 10):
# Pool.apply_async(要調(diào)用的目標,(傳遞給目標的參數(shù)元祖,))
# 每次循環(huán)將會用空閑出來的子進程去調(diào)用目標
po.apply_async(worker, (i,))
print("----start----")
po.close() # 關(guān)閉進程池,關(guān)閉后po不再接收新的請求
po.join() # 等待po中所有子進程執(zhí)行完成,必須放在close語句之后
print("-----end-----")
結(jié)果如下:
multiprocessing.Pool常用函數(shù)解析:
apply_async(func[, args[, kwds]]) :使用非阻塞方式調(diào)用func(并行執(zhí)行,堵塞方式必須等待上一個進程退出才能執(zhí)行下一個進程),args為傳遞給func的參數(shù)列表,kwds為傳遞給func的關(guān)鍵字參數(shù)列表。
close():關(guān)閉Pool,使其不再接受新的任務;
terminate():不管任務是否完成,立即終止;
join():主進程阻塞,等待子進程的退出, 必須在close或terminate之后使用;
apply(func[, args[, kwds]]):使用阻塞方式調(diào)用func。
import multiprocessing
import os, time, random
def worker(msg):
t_start = time.time()
print("%s開始執(zhí)行,進程號為%d" % (msg, os.getpid()))
time.sleep(random.random() * 2)
t_stop = time.time()
print(msg, "執(zhí)行完畢,耗時%0.2f" % (t_stop - t_start))
if __name__ == '__main__':
#print("----start----")
po = multiprocessing.Pool(3)
for i in range(0, 10):
po.apply(worker, (i,))
print("----start----")
po.close()
po.join()
print("-----end-----")
結(jié)果如下:
process之間有時需要通信,操作系統(tǒng)提供了很多機制來實現(xiàn)進程間的通信。以下使用Queue進行通信。
import multiprocessing
q = multiprocessing.Queue(3)
q.put('哈哈')
q.put('嘿嘿')
print(q.full())
q.put('呵呵')
print(q.full())
try:
q.put('第四次', timeout=3)
except:
print('消息隊列已滿,當前消息數(shù)量:%s' % q.qsize())
if not q.empty():
for i in range(q.qsize()):
print(q.get())
結(jié)果如下:
初始化Queue()對象時(例如:q=Queue()),若括號中沒有指定最大可接收的消息數(shù)量,或數(shù)量為負值,那么就代表可接受的消息數(shù)量沒有上限(直到內(nèi)存的盡頭);
Queue.qsize():返回當前隊列包含的消息數(shù)量;
Queue.empty():如果隊列為空,返回True,反之False ;
Queue.full():如果隊列滿了,返回True,反之False;
Queue.get([block[, timeout]]):獲取隊列中的一條消息,然后將其從列隊中移除,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果為空,此時程序?qū)⒈蛔枞ㄍT谧x取狀態(tài)),直到從消息列隊讀到消息為止,如果設置了timeout,則會等待timeout秒,若還沒讀取到任何消息,則拋出"Queue.Empty"異常;
2)如果block值為False,消息列隊如果為空,則會立刻拋出"Queue.Empty"異常;
Queue.get_nowait():相當Queue.get(False);
Queue.put(item,[block[, timeout]]):將item消息寫入隊列,block默認值為True;
1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果已經(jīng)沒有空間可寫入,此時程序?qū)⒈蛔枞ㄍT趯懭霠顟B(tài)),直到從消息列隊騰出空間為止,如果設置了timeout,則會等待timeout秒,若還沒空間,則拋出"Queue.Full"異常;
2)如果block值為False,消息列隊如果沒有空間可寫入,則會立刻拋出"Queue.Full"異常;
Queue.put_nowait(item):相當Queue.put(item, False);
下面在主進程中創(chuàng)建兩個子進程,一個往Queue里寫入數(shù)據(jù),一個從Queue里讀數(shù)據(jù)。
import multiprocessing
import time
import random
# 寫數(shù)據(jù)進程執(zhí)行的代碼:
def write(q):
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
# 讀數(shù)據(jù)進程執(zhí)行的代碼:
def read(q):
while True:
if not q.empty():
value = q.get(True)
print('Get %s from queue.' % value)
time.sleep(random.random())
else:
break
if __name__ == '__main__':
# 主(父)進程創(chuàng)建Queue,并傳給各個子進程:
q = multiprocessing.Queue()
pw = multiprocessing.Process(target=write, args=(q,))
pr = multiprocessing.Process(target=read, args=(q,))
# 啟動子進程pw,寫入:
pw.start()
# 等待pw結(jié)束:
pw.join()
# 啟動子進程pr,讀取:
pr.start()
pr.join()
print('所有數(shù)據(jù)都寫入并且讀完')
結(jié)果如下:
如果要使用Pool創(chuàng)建進程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否則會得到一條如下的錯誤信息:
RuntimeError: Queue objects should only be shared between processes through inheritance.
下面的實例演示了進程池中的進程如何通信
import multiprocessing
import os
def reader(q):
print('reader啟動(%s),父進程為(%s)' % (os.getpid(), os.getppid()))
for i in range(q.qsize()):
print('reader從Queue獲取到消息:%s' % q.get(True))
def writer(q):
print('writer啟動(%s),父進程為(%s)' % (os.getpid(), os.getppid()))
for i in 'ABCD':
q.put(i)
if __name__ == '__main__':
print('(%s) start' % os.getpid())
q = multiprocessing.Manager().Queue() # 使用Manager中的Queue來初始化
po = multiprocessing.Pool()
# 使用阻塞模式創(chuàng)建進程,這樣就不需要在reader中使用死循環(huán)了,可以讓writer完全執(zhí)行完成后,再用reader去讀取
po.apply_async(writer, (q,))
po.apply_async(reader, (q,))
po.close()
po.join()
print('(%s) End' % os.getpid())
結(jié)果如下:
如果使用非阻塞式
po.apply_async(writer, (q,))po.apply_async(reader, (q,))
可能出現(xiàn)如下結(jié)果:
因為cpu的分配不一定誰先誰后,所以可能發(fā)生先進行讀的進程,才開始寫的進程。因此出現(xiàn)了上圖的結(jié)果。
小結(jié)
在Unix/Linux下,可以使用fork()調(diào)用實現(xiàn)多進程。
要實現(xiàn)跨平臺的多進程,可以使用multiprocessing模塊。
進程間通信是通過Queue實現(xiàn)的。