豬行天下之Python基礎——9.3 Python多執行緒與多程式(下)

coder-pig發表於2019-04-13

內容簡述:

  • 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模組提供了四種程式間共享資料的方式:QueueValue和ArrayManager.dict和pipe。下面一一介紹這四種方式的具體用法。

  • 1.Queue佇列

多程式安全的佇列,put方法用以插入資料到佇列中,put方法有兩個可選引數:blockedtimeout若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


如果本文對你有所幫助,歡迎
留言,點贊,轉發
素質三連,謝謝?~


相關文章