內容簡述:
- 1、multiprocess模組詳解
1、multiprocess模組詳解
Python的os模組封裝了常見的系統呼叫,其中就包含 「fork函式」,通過這個函式可以輕鬆的建立子程式,但是要注意一點,在Windows系統上是無法使用fork函式的,Python為我們提供了可跨平臺的multiprocess模組。該模組提供了一個Process類來代表一個程式物件,用法和Thread非常相似。
① Process程式物件
建立一個程式的程式碼示例如下:
from multiprocessing import Process
import os
def show_msg(name):
print("子程式執行中:name = %s , pid = %d " % (name, os.getpid()))
if __name__ == '__main__':
print("父程式 %d" % os.getpid())
p = Process(target=show_msg, args=('測試',))
print("開始執行子程式~")
p.start()
p.join()
print("子程式執行完畢!")
複製程式碼
執行結果如下:
父程式 26539
開始執行子程式~
子程式執行中:name = 測試 , pid = 26540
子程式執行完畢!
複製程式碼
Process建構函式:
Process(group=None, target=None, name=None, args=(), kwargs={})
複製程式碼
引數詳解:
- group:分組,很少用到
- target:呼叫物件,傳入任務執行函式作為引數
- name:程式別名
- args:給呼叫物件以元組的形式提供引數,比如有兩個引數args=(a,b),如果只有一個引數,要這樣寫args=(a,),不能把逗號漏掉,不然會被當做括號運算子使用!
- kwargs:呼叫物件的關鍵字引數字典
Process的常用函式:
- is_alive():判斷程式例項是否還在執行;
- join([timeout]):是否等待程式例項執行結束,或等待多少秒;
- start():啟動程式例項(建立子程式);
- run():如果沒有給定target引數,對這個物件呼叫start()方法時,
就將執行物件中的run()方法;- terminate():不管任務是否完成,立即終止;
除了使用fork函式和上述操作建立程式的方式外,還可以自定義一個Process類,重寫__init__
和run函式
即可,程式碼示例如下:
from multiprocessing import Process
import os
class MyProcess(Process):
def __init__(self, name):
Process.__init__(self)
self.msg = name
def run(self):
print("子程式執行中:name = %s , pid = %d " % (self.msg, os.getpid()))
if __name__ == '__main__':
print("父程式 %d" % os.getpid())
p = MyProcess('測試')
print("開始執行子程式~")
p.start()
p.join()
print("子程式執行完畢!")
複製程式碼
執行結果如下:
父程式 26794
開始執行子程式~
子程式執行中:name = 測試 , pid = 26795
子程式執行完畢!
複製程式碼
② Pool程式池
知道了如何建立程式,那麼實現多程式有不是什麼難事了,一個迴圈建立多個即可,但是有個問題,程式可是重量級別的程式,重複程式建立和銷燬會造成一定的效能開銷! Python為我們提供了一個程式池物件Pool用來緩解程式重複關啟帶來的效能消耗問題。在建立程式池的時候指定一個容量,如果接收到一個新任務,而池沒滿的話,會建立一個新的程式來執行這個任務,如果池滿的話,任務則會進入等待狀態,直到池中有程式結束,才會建立新的程式來執行這個任務。
Pool的建構函式:
Pool(processes=None, initializer=None, initargs=(),maxtasksperchild=None, context=None)
複製程式碼
一般只用到第一個引數,processes用於設定程式池的容量,即最多併發的程式數量,如果不寫預設使用os.cpu_count()
返回的值。
Pool常用函式詳解:
- apply(func, args=(), kwds={}):使用堵塞方式呼叫func,堵塞的意思是一個程式結束,
釋放回程式池,下一個程式才可以開始,args為傳遞給func的引數列表,kwds為傳遞給
func的關鍵字引數列表,該方法在Python 2.3後就不建議使用了。- apply_async(func, args=(), kwds={}, callback=None,error_callback=None) :使用非阻塞方式呼叫func,程式池程式最大數可以同時執行,還支援返回結果後進行回撥。
- close():關閉程式池,使其不再接受新的任務;
- terminate():結束工作程式,不再處理未處理的任務,不管任務是否完成,立即終止;
- join():主程式阻塞,等待⼦程式的退出,必須在close或terminate之後使用;
- map(func, iterable, chunksize=None):這裡的map函式和Python內建的高階函式map類似,只是這裡的map方法是在程式池多程式併發進行的,接收一個函式和可迭代物件,把函式作用到每個元素,得到一個新的list返回。
最簡單的程式池程式碼示例如下:
import multiprocessing as mp
import time
def func(msg):
time.sleep(1)
print(mp.current_process().name + " : " + msg)
if __name__ == '__main__':
pool = mp.Pool()
for i in range(20):
msg = "Do Something %d" % (i)
pool.apply_async(func, (msg,))
pool.close()
pool.join()
print("子程式執行任務完畢!")
複製程式碼
執行結果如下:
ForkPoolWorker-4 : Do Something 3
ForkPoolWorker-2 : Do Something 1
ForkPoolWorker-1 : Do Something 0
ForkPoolWorker-3 : Do Something 2
ForkPoolWorker-5 : Do Something 4
ForkPoolWorker-6 : Do Something 5
ForkPoolWorker-7 : Do Something 6
ForkPoolWorker-8 : Do Something 7
ForkPoolWorker-2 : Do Something 9
ForkPoolWorker-4 : Do Something 8
ForkPoolWorker-1 : Do Something 11
ForkPoolWorker-7 : Do Something 12
ForkPoolWorker-5 : Do Something 13
ForkPoolWorker-6 : Do Something 14
ForkPoolWorker-3 : Do Something 10
ForkPoolWorker-8 : Do Something 15
ForkPoolWorker-6 : Do Something 19
ForkPoolWorker-1 : Do Something 17
ForkPoolWorker-5 : Do Something 18
ForkPoolWorker-7 : Do Something 16
子程式執行任務完畢!
複製程式碼
上面的輸出結果順序並沒有按照迴圈中的順序輸出,可以利用apply_async
的返回值是:被程式呼叫的函式的返回值,來規避,修改後的程式碼如下:
import multiprocessing as mp
import time
def func(msg):
time.sleep(1)
return mp.current_process().name + " : " + msg
if __name__ == '__main__':
pool = mp.Pool()
results = []
for i in range(20):
msg = "Do Something %d" % i
results.append(pool.apply_async(func, (msg,)))
pool.close()
pool.join()
for result in results:
print(result.get())
print("子程式執行任務完畢!")
複製程式碼
執行結果如下:
ForkPoolWorker-1 : Do Something 0
ForkPoolWorker-2 : Do Something 1
ForkPoolWorker-3 : Do Something 2
ForkPoolWorker-4 : Do Something 3
ForkPoolWorker-5 : Do Something 4
ForkPoolWorker-7 : Do Something 6
ForkPoolWorker-6 : Do Something 5
ForkPoolWorker-8 : Do Something 7
ForkPoolWorker-1 : Do Something 8
ForkPoolWorker-2 : Do Something 9
ForkPoolWorker-4 : Do Something 11
ForkPoolWorker-3 : Do Something 10
ForkPoolWorker-7 : Do Something 12
ForkPoolWorker-8 : Do Something 13
ForkPoolWorker-5 : Do Something 14
ForkPoolWorker-6 : Do Something 15
ForkPoolWorker-1 : Do Something 16
ForkPoolWorker-2 : Do Something 17
ForkPoolWorker-4 : Do Something 18
ForkPoolWorker-3 : Do Something 19
子程式執行任務完畢!
複製程式碼
感覺還是有點模糊,通過一個多程式統計目錄下檔案的行數和字元個數的指令碼來鞏固,程式碼示例如下:
import multiprocessing as mp
import time
import os
result_file = 'result.txt' # 統計結果寫入檔名
# 獲得路徑下的檔案列表
def get_files(path):
file_list = []
for file in os.listdir(path):
if file.endswith('py'):
file_list.append(os.path.join(path, file))
return file_list
# 統計每個檔案中函式與字元數
def get_msg(path):
with open(path, 'r', encoding='utf-8') as f:
content = f.readlines()
f.close()
lines = len(content)
char_count = 0
for i in content:
char_count += len(i.strip("\n"))
return lines, char_count, path
# 將資料寫入到檔案中
def write_result(result_list):
with open(result_file, 'a', encoding='utf-8') as f:
for result in result_list:
f.write(result[2] + " 行數:" + str(result[0]) + " 字元數:" + str(result[1]) + "\n")
f.close()
if __name__ == '__main__':
start_time = time.time()
file_list = get_files(os.getcwd())
pool = mp.Pool()
result_list = pool.map(get_msg, file_list)
pool.close()
pool.join()
write_result(result_list)
print("處理完畢,用時:", time.time() - start_time)
複製程式碼
執行結果如下:
# 控制檯輸出
處理完畢,用時: 0.13662314414978027
# result.txt檔案內容
/Users/jay/Project/Python/Book/Chapter 11/11_4.py 行數:33 字元數:621
/Users/jay/Project/Python/Book/Chapter 11/11_1.py 行數:32 字元數:578
/Users/jay/Project/Python/Book/Chapter 11/11_5.py 行數:52 字元數:1148
/Users/jay/Project/Python/Book/Chapter 11/11_13.py 行數:20 字元數:333
/Users/jay/Project/Python/Book/Chapter 11/11_16.py 行數:62 字元數:1320
/Users/jay/Project/Python/Book/Chapter 11/11_12.py 行數:23 字元數:410
/Users/jay/Project/Python/Book/Chapter 11/11_15.py 行數:48 字元數:1087
/Users/jay/Project/Python/Book/Chapter 11/11_8.py 行數:17 字元數:259
/Users/jay/Project/Python/Book/Chapter 11/11_11.py 行數:18 字元數:314
/Users/jay/Project/Python/Book/Chapter 11/11_10.py 行數:46 字元數:919
/Users/jay/Project/Python/Book/Chapter 11/11_14.py 行數:20 字元數:401
/Users/jay/Project/Python/Book/Chapter 11/11_9.py 行數:31 字元數:623
/Users/jay/Project/Python/Book/Chapter 11/11_2.py 行數:32 字元數:565
/Users/jay/Project/Python/Book/Chapter 11/11_6.py 行數:23 字元數:453
/Users/jay/Project/Python/Book/Chapter 11/11_7.py 行數:37 字元數:745
/Users/jay/Project/Python/Book/Chapter 11/11_3.py 行數:29 字元數:518
複製程式碼
③ 程式間共享資料
涉及到了多個程式,不可避免的要處理程式間資料交換問題,多程式不像多執行緒,不同程式之間記憶體是不共享的,multiprocessing模組提供了四種程式間共享資料的方式:Queue,Value和Array,Manager.dict和pipe。下面一一介紹這四種方式的具體用法。
- 1.Queue佇列
多程式安全的佇列,put方法用以插入資料到佇列中,put方法有兩個可選引數:blocked和timeout。若blocked為True(預設)且timeout為正值,該方法會阻塞timeout指定的時間,直到該佇列有剩餘的空間。如果超時,會丟擲Queue.Full異常。如果blocked為False,但該Queue已滿,會立即丟擲Queue.Full異常,而get方法則從佇列讀取並且刪除一個元素,引數規則同丟擲的一場是Queue.Empty。另外Queue不止適用於程式通訊,也適用於執行緒,順道寫一個比較單執行緒,多執行緒
和多程式的執行效率對比示例,具體程式碼如下:
import threading as td
import multiprocessing as mp
import time
def do_something(queue):
result = 0
for i in range(100000):
result += i ** 2
queue.put(result)
# 單執行緒
def normal():
result = 0
for _ in range(3):
for i in range(100000):
result += i ** 2
print("單執行緒處理結果:", result)
# 多執行緒
def multi_threading():
q = mp.Queue()
t1 = td.Thread(target=do_something, args=(q,))
t2 = td.Thread(target=do_something, args=(q,))
t3 = td.Thread(target=do_something, args=(q,))
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print("多執行緒處理結果:", (q.get() + q.get() + q.get()))
# 多程式
def multi_process():
q = mp.Queue()
p1 = mp.Process(target=do_something, args=(q,))
p2 = mp.Process(target=do_something, args=(q,))
p3 = mp.Process(target=do_something, args=(q,))
p1.start()
p2.start()
p3.start()
p1.join()
p2.join()
p3.join()
print("多程式處理結果:", (q.get() + q.get() + q.get()))
if __name__ == '__main__':
start_time_1 = time.time()
normal()
start_time_2 = time.time()
print("單執行緒處理耗時:", start_time_2 - start_time_1)
multi_threading()
start_time_3 = time.time()
print("多執行緒處理耗時:", start_time_3 - start_time_2)
multi_process()
start_time_4 = time.time()
print("多繼承處理耗時:", start_time_4 - start_time_3)
複製程式碼
執行結果如下:
單執行緒處理結果: 999985000050000
單執行緒處理耗時: 0.10726284980773926
多執行緒處理結果: 999985000050000
多執行緒處理耗時: 0.13849401473999023
多程式處理結果: 999985000050000
多繼承處理耗時: 0.041596174240112305
複製程式碼
從上面的結果可以明顯看出在處理CPU密集型任何時,多程式更優。
- 2.Value和Array
兩者是通過「共享記憶體」的方式來共享資料的,前者用於需要共享單個值,後者用於
共享多個值(陣列),建構函式的第一個元素為資料型別,第二個元素為值。資料型別對照如表所示。
標記 | 資料型別 | 標記 | 資料型別 |
---|---|---|---|
'c' | ctypes.c_char | 'u' | ctypes.c_wchar |
'b' | ctypes.c_byte | 'B' | ctypes.c_ubyte |
'h' | ctypes.c_short | 'H' | ctypes.c_ushort |
'i' | ctypes.c_int | 'I' | ctypes.c_uint |
'l' | ctypes.c_long | 'L' | ctypes.c_ulong |
'f' | ctypes.c_float | 'd' | ctypes.c_double |
使用程式碼示例如下:
import multiprocessing as mp
def do_something(num, arr):
num.value += 1
for i in range(len(arr)):
arr[i] = arr[i] * 2
if __name__ == '__main__':
value = mp.Value('i', 1)
array = mp.Array('i', range(5))
print("剛開始的值:", value.value, array[:])
# 建立程式1
p1 = mp.Process(target=do_something, args=(value, array))
p1.start()
p1.join()
print("程式1操作後的值:", value.value, array[:])
# 建立程式2
p2 = mp.Process(target=do_something, args=(value, array))
p2.start()
p2.join()
print("程式2操作後的值:", value.value, array[:])
複製程式碼
執行結果如下:
剛開始的值: 1 [0, 1, 2, 3, 4]
程式1操作後的值: 2 [0, 2, 4, 6, 8]
程式2操作後的值: 3 [0, 4, 8, 12, 16]
複製程式碼
- 3.Manager
Python還為我們提供更加強大的資料共享類,支援更豐富的資料型別,比如Value、Array、dict、list、Lock、Semaphore等等,另外Manager還可以共享類的例項物件。有一點要注意:程式間通訊應該儘量避免使用共享資料的方式!
使用程式碼示例如下:
import multiprocessing as mp
import os
import time
def do_something(dt):
dt[os.getpid()] = int(time.time())
print(data_dict)
if __name__ == '__main__':
manager = mp.Manager()
data_dict = manager.dict()
for i in range(3):
p=mp.Process(target=do_something,args=(data_dict,))
p.start()
p.join()
複製程式碼
執行結果如下:
{5432: 1533200189}
{5432: 1533200189, 5433: 1533200189}
{5432: 1533200189, 5433: 1533200189, 5434: 1533200189}
複製程式碼
- 4.Pipe
管道,簡化版的Queue,通過Pipe()建構函式可以建立一個程式通訊用的管道物件,預設雙向,意味著使用管道只能同時開啟兩個程式!如果想設定單向,可以新增引數「duplex=False」,雙向即可傳送也可接受,但是隻允許前面的埠用於接收,後面的埠用於傳送。管道物件傳送和接收資訊的函式依次為send()和recv()。使用程式碼示例如下:
import multiprocessing as mp
def p_1(p):
p.send("你好啊!")
print("P1-收到資訊:", p.recv())
def p_2(p):
print("P2-收到資訊:", p.recv())
p.send("你也好啊!")
if __name__ == '__main__':
pipe = mp.Pipe()
p1 = mp.Process(target=p_1, args=(pipe[0],))
p2 = mp.Process(target=p_2, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
複製程式碼
執行結果如下:
P2-收到資訊: 你好啊!
P1-收到資訊: 你也好啊!
複製程式碼
關於多程式加鎖的,可以參見前面多程式加鎖部分內容,這裡就不重複講解了,只是導包換成了multiprocessing,比如threading.Lock換成了multiprocessing.Lock。
如果本文對你有所幫助,歡迎
留言,點贊,轉發
素質三連,謝謝?~