Python多程式程式設計

昀溪發表於2018-08-08

 

本文大綱

  1. 程式
  2. 程式池
  3. 程式間通訊
  4. 回撥函式
  5. 程式鎖

程式

之前一篇文章介紹了多執行緒程式設計,我們知道python的多執行緒適合IO密集型程式設計,對於計算密集型不太適合因為無法做到真正的併發,所以如果在一個計算密集型場景下還是使用多程式會更好。首先我們先看看如何使用一個程式。

(例子1)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import multiprocessing
from atexit import register


def task(name):
    print("我是子程式名稱:" + p.name + " PID: " + str(p.pid))
    time.sleep(1)


# 加這個裝飾器是為了在主程式退出 main 函式之前呼叫
@register
def _atexit():
    print("主程式程式碼與子程式均執行完畢,主程式即將退出。")


if __name__ == '__main__':
    for i in range(5):
        # 程式開啟必須放在main()下, name 是這個子程式的名稱,可以不寫。
        p = multiprocessing.Process(target=task, args=(i,))

        # 是否設定子程式為守護程式,預設是False,用法與含義同執行緒
        # p.daemon = True
        p.start()

        # join 主執行緒阻塞等待子程式執行完畢後再繼續執行,如果使用這個就不是併發的,可以設定一個超時
        # 時間,超過這個時間子程式沒有完成,該函式也會返回繼續執行主執行緒,如果程式不會爭搶資源
        # 可以不設定阻塞。
        # p.join(1)

    # print "CPU數量:" + str(multiprocessing.cpu_count())

    print('主程式程式碼執行完畢。')

其實單一的這樣使用一個子程式沒有多大意義,這裡也只做說明,它的用法其實和使用執行緒是類似的。下面我們看看程式池。

程式池

程式池就是每次每次允許併發的程式不能超過池子的大小,其實這也是為了控制程式數量。如果需要的程式數量大於池數量,那麼也只能按照池子數量來建立程式,當池子中的程式執行完畢空閒一個位置之後,再向池子放一個程式進來。程式池大小不要超過CPU核心數量,不是說不能這樣做,而是不規範,作業系統本身也需要CPU核心,所以要留一個給OS使用。尤其是當程式多計算任務量大,如果你不給OS留一個核心,系統本身也就慢了。

(例子2)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
from multiprocessing import Pool
from multiprocessing import cpu_count
import os


def task(name):
    time.sleep(3)
    print("子程式名稱:" + name, "子程式ID:", os.getpid(), "父程式ID:", os.getppid())


if __name__ == '__main__':
    # 獲取CPU數量
    poolZize = cpu_count() - 1
    # 建立程式池,並設定大小,預設是你主機的CPU核心數量,這個大小可以隨意,但是不要超過CPU核心數量
    p = Pool(poolZize)
    print("主程式ID:", os.getpid())

    for i in range(5):
        # 非阻塞程式
        p.apply_async(task, args=(str(i),))

    # 呼叫close之後就不能再往池裡新增程式了,如果使用死迴圈這裡就不能呼叫這個方法,因為呼叫後第二次迴圈就無法向池子裡新增程式了。
    p.close()
    # 對 pool 呼叫join必須先呼叫 close(),這裡的join的意思是等待所有子程式執行完畢,然後再執行主執行緒下面的程式碼,直到退出程式
    p.join()
    time.sleep(1)
    print("主執行緒程式碼執行完畢。")

如果我們要關注每個程式執行結果怎麼辦?程式碼稍加改變。

(例子3)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
from multiprocessing import Pool
from multiprocessing import cpu_count
import os


def task(name):
    time.sleep(3)
    print("子程式名稱:" + name, "子程式ID:", os.getpid(), "父程式ID:", os.getppid())
    # 新增一個返回值表示每個子程式任務的執行結果
    return {name: 'Job is done'}


if __name__ == '__main__':
    processlist = []
    # 獲取CPU數量
    poolZize = cpu_count() - 1
    # 建立程式池,並設定大小,預設是你主機的CPU核心數量,這個大小可以隨意,但是不要超過CPU核心數量
    p = Pool(poolZize)
    print("主程式ID:", os.getpid())

    for i in range(5):
        # 把程式新增到列表中
        processlist.append(p.apply_async(task, args=(str(i),)))

    # 呼叫close之後就不能再往池裡新增程式了,如果使用死迴圈這裡就不能呼叫這個方法,因為呼叫後第二次迴圈就無法向池子裡新增程式了。
    p.close()
    # 對 pool 呼叫join必須先呼叫 close(),這裡的join的意思是等待所有子程式執行完畢,然後再執行主執行緒下面的程式碼,直到退出程式
    p.join()
    time.sleep(1)

    # 用於獲取執行結果
    for i in processlist:
        print(i.get())
    print("主執行緒程式碼執行完畢。")

程式間通訊

程式間通訊英文簡寫是IPC,它其實有多種方式,比如古老的管道、佇列、訊號量、共享記憶體。我們下面使用的你可以理解為佇列。

程式間通訊的實現和之前執行緒的通訊類似都是使用一個佇列,只是這裡不是原來的佇列,而是Manager裡面的。

我這裡的例子比較簡單就是每個程式把自己獲得的數新增到佇列裡面,然後在主程式中讀取。

Queue

(例子4)

 1 #!/usr/bin/env python
 2 # -*- coding: utf-8 -*-
 3 
 4 """
 5 程式之間進行通訊,不同程式可以同時訪問一個物件
 6 """
 7 
 8 import sys, time
 9 from multiprocessing import Queue, Process
10 
11 
12 def fun1(q, i):
13     q.put(i)
14 
15 
16 # 透過Queue()來實現程式間通訊
17 def method1():
18     queue = Queue()
19     pList = []
20     for i in range(10):
21         p = Process(target=fun1, args=(queue, i,))
22         pList.append(p)
23         p.start()
24 
25     for p in pList:
26         p.join()
27 
28     while not queue.empty():
29         print(queue.get())
30 
31 
32 def main():
33     try:
34         method1()
35     except Exception as err:
36         print(err)
37 
38 
39 if __name__ == "__main__":
40     try:
41         main()
42     finally:
43         sys.exit()

說明:程式間通訊使用的這個Queue()佇列並不是共享的,而是把佇列例項序列化後傳給其他程式,其他程式反序列化後再使用。而執行緒中使用的佇列是共享的,因為執行緒本身就是共享記憶體的。那麼如何共享呢?就要用到下面這個。

Manager

Manager支援很多型別,字典、列表、佇列、鎖等。而且它本身就對資料進行加鎖,你不用手動加鎖。程式的通訊的共享資料可以用Manager所支援的任何型別,比如我們使用列表,如下:

(例子5)

 1 #!/usr/bin/env python
 2 # -*- coding: utf-8 -*-
 3 
 4 """
 5 程式之間進行通訊,不同程式可以同時訪問一個物件
 6 """
 7 
 8 import sys, time
 9 from multiprocessing import Manager, Process
10 
11 
12 def fun1(list1, i):
13     list1.append(i)
14 
15 
16 # Manager().Queue()   可共享的佇列
17 # Manager().dict()    可共享的字典
18 # Manager().list()    可共享的列表
19 # Manager().Lock()    可共享的鎖
20 
21 def method1():
22     # 定義可共享的列表
23     mList = Manager().list()
24     pList = []
25     for i in range(5):
26         p = Process(target=fun1, args=(mList, i,))
27         pList.append(p)
28         p.start()
29 
30     for p in pList:
31         p.join()
32 
33     print(mList)
34 
35 
36 def main():
37     try:
38         method1()
39     except Exception as err:
40         print(err)
41 
42 
43 if __name__ == "__main__":
44     try:
45         main()
46     finally:
47         sys.exit()

其實原則上程式無法共享資料Manager其實也不是,什麼是共享?資料只有一份,大家都能讀取。程式的資料傳遞實際上是序列化之後傳送給對方,這樣同一個資料就有2份,A程式發給B,B修改完之後再發給A,A拿到後更新自己的,就這麼回事。到底程式能不能共享資料,其實這就是程式間通訊的幾種方式,最接近共享的MMAP也就是記憶體對映,把A程式的記憶體地址影響到B程式中。

下面再給出一個程式池內程式共享資料的例子我這裡的例子比較簡單就是每個程式把自己獲得的數新增到佇列裡面,然後在主執行緒中讀取。

(例子6)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
程式之間進行通訊,不同程式可以同時訪問一個物件
Manager這裡面支援很多型別,比如 字典、列表、Queue、鎖等共享.
"""

import sys, time
from multiprocessing import Manager, Pool


def fun1(q, i):
    q.put(i)


# 透過Manager().Queue()來實現程式共享的一個佇列
def method1():
    queue = Manager().Queue()

    pool = Pool(3)
    for i in range(10):
        pool.apply_async(fun1, args=(queue, i,))
    pool.close()
    pool.join()

    while not queue.empty():
        print(queue.get())

def main():
    method1()
    # method2()


if __name__ == "__main__":
    try:
        main()
    finally:
        sys.exit()

說明:from multiprocessing import Queue,這個Queue不能用在程式池裡,在程式池中只能使用Manager()支援的型別。

回撥函式

非同步程式池可以設定一個回撥函式,就是子程式執行完畢後回撥該函式進行執行。需要注意的是這個回撥函式是主程式呼叫的不是子程式呼叫的

(例子7)

 1 #!/usr/bin/env python
 2 # -*- coding: utf-8 -*-
 3 
 4 """
 5 程式之間進行通訊,不同程式可以同時訪問一個物件
 6 Manager這裡面支援很多型別,比如 字典、列表、Queue、鎖等共享.
 7 """
 8 
 9 import sys, time
10 from multiprocessing import Manager, Pool
11 
12 import os
13 
14 
15 def fun1(i):
16     print(os.getpid())
17 
18 
19 def cbFun(arg):
20     print("   ---> I am done.", arg)
21 
22 
23 def method1():
24     pool = Pool(3)
25     for i in range(3):
26         # 程式執行完fun1後,執行callback設定的函式
27         pool.apply_async(fun1, args=(i,), callback=cbFun("A"))
28     pool.close()
29     pool.join()
30 
31 
32 def main():
33     method1()
34 
35 
36 if __name__ == "__main__":
37     try:
38         main()
39     finally:
40         sys.exit()

回撥幹嘛用呢?回撥能幹的事情直接寫在子程式程式碼裡不也行嘛。我相信很多人都有這樣的疑問,的確是這樣,從功能角度講沒問題可以這樣做,但是從效能用回撥可能會更好,比如每個程式去做某些事情,完成之後需要向資料庫插入一條記錄表示該程式完成了這個工作。這時候向資料庫寫資料這個功能你可以放在子程式裡也可以放在回撥裡,但如果放在子程式裡那就是N個資料庫連線,放在回撥裡則可以是1個資料庫連線,你可以把這個功能做成一個類,初始化的時候就建立好連線,寫資料的時候呼叫該例項的方法就可以。這樣1個連線的開銷肯定比N個連線要小。

還有就是回撥函式其實是把函式當做引數由主程式去執行,如果你在子程式中執行回撥函式的功能相當於硬編碼畫蛇添足,因為子程式本身有特定的任務,上面提到寫入資料庫,如果需求變了要求寫入檔案或者做其他的事情,那麼你的子程式程式碼就要修改,透過回撥函式就可以解耦,回撥函式的函式名稱不變至於裡面幹什麼無所謂。還有一種情況就是你寫一個功能讓別人使用至於只能功能完成之後的是否做其他事情以及怎麼做不是你能控制的,就是AJAX中的常常用到回撥函式也就是執行完AJAX呼叫之後還要做什麼由使用人來控制。在你編寫程式的時候函式或者模組功能要單一化,所有的程式碼都是圍繞這個功能寫的。迅雷大家都用過,下載完成後會有提示音或者可以設定下載後關機這些動作,假設你設定了下載完成後關機,難道你要把關機功能寫到下載功能裡面去嗎,顯然不能。可是關機模組怎麼知道下載完了呢?這時候就用到回撥函式,下載完成後回撥關機功能。

程式鎖

程式本來就是獨立的互不影響,那還要鎖幹嘛?鎖的目的就是為了避免資源爭搶,雖然資料是每個程式獨立的,但是還有其他資源,比如顯示器。需要注意在程式池的程式中不能用鎖。

(例子8)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
程式之間進行通訊,不同程式可以同時訪問一個物件
"""

import sys, time
from multiprocessing import Manager, Process, Lock


def fun1(lock, i):
    print("Hello world ", i)


def method1():
    pList = []
    # lock = Lock()
    for i in range(50):
        p = Process(target=fun1, args=(lock, i,))
        pList.append(p)
        p.start()

    for p in pList:
        p.join()


def main():
    try:
        method1()
    except Exception as err:
        print(err)


if __name__ == "__main__":
    try:
        main()
    finally:
        sys.exit()

輸出就會發生混亂,某個程式還沒有列印完畢另外一個程式也來列印。我們加鎖來看,使用方式和執行緒一樣。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
程式之間進行通訊,不同程式可以同時訪問一個物件
"""

import sys, time
from multiprocessing import Manager, Process, Lock


def fun1(lock, i):
    lock.acquire()
    print("Hello world ", i)
    lock.release()


def method1():
    pList = []
    lock = Lock()
    for i in range(50):
        p = Process(target=fun1, args=(lock, i,))
        pList.append(p)
        p.start()

    for p in pList:
        p.join()


def main():
    try:
        method1()
    except Exception as err:
        print(err)


if __name__ == "__main__":
    try:
        main()
    finally:
        sys.exit()

這樣在執行就不會出現錯亂的問題。

程式池中的其他說明

程式池中的程式同步類的東西要使用Manager裡面的,比如dic、lock等。如下圖:

也就是說在程式池中的東西都是獨有的,不過用法和執行緒的都一樣。不過要注意使用Manager裡面的資料比如dict、list、queue這些雖然可以共享資料但是要注意資料同步,你可能需要加鎖。

相關文章