Python多程式程式設計基礎——圖文版

老錢發表於2018-05-27

多程式程式設計知識是Python程式設計師進階高階的必備知識點,我們平時習慣了使用multiprocessing庫來操縱多程式,但是並不知道它的具體實現原理。下面我對多程式的常用知識點都簡單列了一遍,使用原生的多程式方法呼叫,幫助讀者理解多程式的實現機制。程式碼跑在linux環境下。沒有linux條件的,可以使用docker或者虛擬機器執行進行體驗。

docker pull python:2.7
複製程式碼

生成子程式

Python生成子程式使用os.fork(),它將產生一個子程式。fork呼叫同時在父程式和主程式同時返回,在父程式中返回子程式的pid,在子程式中返回0,如果返回值小於零,說明子程式產生失敗,一般是因為作業系統資源不足。

Python多程式程式設計基礎——圖文版

import os

def create_child():
    pid = os.fork()
    if pid > 0:
        print 'in father process'
        return True
    elif pid == 0:
        print 'in child process'
        return False
    else:
        raise
複製程式碼

生成多個子程式

我們呼叫create_child方法多次就可以生成多個子程式,前提是必須保證create_child是在父程式裡執行,如果是子程式,就不要在呼叫了。

Python多程式程式設計基礎——圖文版

# coding: utf-8
# child.py
import os

def create_child(i):
    pid = os.fork()
    if pid > 0:
        print 'in father process'
        return pid
    elif pid == 0:
        print 'in child process', i
        return 0
    else:
        raise

for i in range(10):  # 迴圈10次,建立10個子程式
    pid = create_child(i)
    # pid==0是子程式,應該立即退出迴圈,否則子程式也會繼續生成子程式
    # 子子孫孫,那就生成太多程式了
    if pid == 0:
        break
複製程式碼

執行python child.py,輸出

in father process
in father process
in child process 0
in child process 1
in father process
in child process 2
in father process
in father process
in child process 3
in father process
in child process 4
in child process 5
in father process
in father process
in child process 6
in child process 7
in father process
in child process 8
in father process
in child process 9
複製程式碼

程式休眠

使用time.sleep可以使程式休眠任意時間,單位為秒,可以是小數

Python多程式程式設計基礎——圖文版

import time

for i in range(5):
    print 'hello'
    time.sleep(1)  # 睡1s
複製程式碼

殺死子程式

使用os.kill(pid, sig_num)可以向程式號為pid的子程式傳送訊號,sig_num常用的有SIGKILL(暴力殺死,相當於kill -9),SIGTERM(通知對方退出,相當於kill不帶引數),SIGINT(相當於鍵盤的ctrl+c)。

Python多程式程式設計基礎——圖文版

# coding: utf-8
# kill.py

import os
import time
import signal


def create_child():
    pid = os.fork()
    if pid > 0:
        return pid
    elif pid == 0:
        return 0
    else:
        raise


pid = create_child()
if pid == 0:
    while True:  # 子程式死迴圈列印字串
        print 'in child process'
        time.sleep(1)
else:
    print 'in father process'
    time.sleep(5)  # 父程式休眠5s再殺死子程式
    os.kill(pid, signal.SIGKILL)
    time.sleep(5)  # 父程式繼續休眠5s觀察子程式是否還有輸出
複製程式碼

執行python kill.py,我們看到控制檯輸出如下

in father process
in child process
# 等1s
in child process
# 等1s
in child process
# 等1s
in child process
# 等1s
in child process
# 等了5s
複製程式碼

說明os.kill執行之後,子程式已經停止輸出了

殭屍子程式

在上面的例子中,os.kill執行完之後,我們通過ps -ef|grep python快速觀察程式的狀態,可以發現子程式有一個奇怪的顯示<defunct>

root        12     1  0 11:22 pts/0    00:00:00 python kill.py
root        13    12  0 11:22 pts/0    00:00:00 [python] <defunct>
複製程式碼

待父程式終止後,子程式也一塊消失了。那<defunct>是什麼含義呢? 它的含義是「殭屍程式」。子程式結束後,會立即成為殭屍程式,殭屍程式佔用的作業系統資源並不會立即釋放,它就像一具屍體啥事也不幹,但是還是持續佔據著作業系統的資源(記憶體等)。

收割子程式

如果徹底幹掉殭屍程式?父程式需要呼叫waitpid(pid, options)函式,「收割」子程式,這樣子程式才可以灰飛煙滅。waitpid函式會返回子程式的退出狀態,它就像子程式留下的臨終遺言,必須等父程式聽到後才能徹底瞑目。

Python多程式程式設計基礎——圖文版

# coding: utf-8

import os
import time
import signal


def create_child():
    pid = os.fork()
    if pid > 0:
        return pid
    elif pid == 0:
        return 0
    else:
        raise


pid = create_child()
if pid == 0:
    while True:  # 子程式死迴圈列印字串
        print 'in child process'
        time.sleep(1)
else:
    print 'in father process'
    time.sleep(5)  # 父程式休眠5s再殺死子程式
    os.kill(pid, signal.SIGTERM)
    ret = os.waitpid(pid, 0)  # 收割子程式
    print ret  # 看看到底返回了什麼
    time.sleep(5)  # 父程式繼續休眠5s觀察子程式是否還存在
複製程式碼

執行python kill.py輸出如下

in father process
in child process
in child process
in child process
in child process
in child process
in child process
(125, 9)
複製程式碼

我們看到waitpid返回了一個tuple,第一個是子程式的pid,第二個9是什麼含義呢,它在不同的作業系統上含義不盡相同,不過在Unix上,它通常的value是一個16位的整數值,前8位表示程式的退出狀態,後8位表示導致程式退出的訊號的整數值。所以本例中退出狀態位0,訊號編號位9,還記得kill -9這個命令麼,就是這個9表示暴力殺死程式。

如果我們將os.kill換一個訊號才看結果,比如換成os.kill(pid, signal.SIGTERM),可以看到返回結果變成了(138, 15),15就是SIGTERM訊號的整數值。

waitpid(pid, 0)還可以起到等待子程式結束的功能,如果子程式不結束,那麼該呼叫會一直卡住。

捕獲訊號

SIGTERM訊號預設處理動作就是退出程式,其實我們還可以設定SIGTERM訊號的處理函式,使得它不退出。

# coding: utf-8

import os
import time
import signal


def create_child():
    pid = os.fork()
    if pid > 0:
        return pid
    elif pid == 0:
        return 0
    else:
        raise


pid = create_child()
if pid == 0:
    signal.signal(signal.SIGTERM, signal.SIG_IGN)
    while True:  # 子程式死迴圈列印字串
        print 'in child process'
        time.sleep(1)
else:
    print 'in father process'
    time.sleep(5)  # 父程式休眠5s再殺死子程式
    os.kill(pid, signal.SIGTERM)  # 發一個SIGTERM訊號
    time.sleep(5)  # 父程式繼續休眠5s觀察子程式是否還存在
    os.kill(pid, signal.SIGKILL)  # 發一個SIGKILL訊號
    time.sleep(5)  # 父程式繼續休眠5s觀察子程式是否還存在
複製程式碼

我們在子程式裡設定了訊號處理函式,SIG_IGN表示忽略訊號。我們發現第一次呼叫os.kill之後,子程式會繼續輸出。說明子程式沒有被殺死。第二次os.kill之後,子程式終於停止了輸出。

Python多程式程式設計基礎——圖文版

接下來我們換一個自定義訊號處理函式,子程式收到SIGTERM之後,列印一句話再退出。

# coding: utf-8

import os
import sys
import time
import signal


def create_child():
    pid = os.fork()
    if pid > 0:
        return pid
    elif pid == 0:
        return 0
    else:
        raise


def i_will_die(sig_num, frame):  # 自定義訊號處理函式
    print "child will die"
    sys.exit(0)


pid = create_child()
if pid == 0:
    signal.signal(signal.SIGTERM, i_will_die)
    while True:  # 子程式死迴圈列印字串
        print 'in child process'
        time.sleep(1)
else:
    print 'in father process'
    time.sleep(5)  # 父程式休眠5s再殺死子程式
    os.kill(pid, signal.SIGTERM)
    time.sleep(5)  # 父程式繼續休眠5s觀察子程式是否還存在
複製程式碼

輸出如下

in father process
in child process
in child process
in child process
in child process
in child process
child will die
複製程式碼

訊號處理函式有兩個引數,第一個sig_num表示被捕獲訊號的整數值,第二個frame不太好理解,一般也很少用。它表示被訊號打斷時,Python的執行的棧幀物件資訊。讀者可以不必深度理解。

多程式平行計算例項

下面我們使用多程式進行一個計算圓周率PI。對於圓周率PI有一個數學極限公式,我們將使用該公司來計算圓周率PI。

Python多程式程式設計基礎——圖文版

先使用單程式版本

import math

def pi(n):
    s = 0.0
    for i in range(n):
        s += 1.0/(2*i+1)/(2*i+1)
    return math.sqrt(8 * s)

print pi(10000000)
複製程式碼

輸出

3.14159262176
複製程式碼

這個程式跑了有一小會才出結果,不過這個值已經非常接近圓周率了。

Python多程式程式設計基礎——圖文版

接下來我們用多程式版本,我們用redis進行程式間通訊。

# coding: utf-8

import os
import sys
import math
import redis


def slice(mink, maxk):
    s = 0.0
    for k in range(mink, maxk):
        s += 1.0/(2*k+1)/(2*k+1)
    return s


def pi(n):
    pids = []
    unit = n / 10
    client = redis.StrictRedis()
    client.delete("result")  # 保證結果集是乾淨的
    del client  # 關閉連線
    for i in range(10):  # 分10個子程式
        mink = unit * i
        maxk = mink + unit
        pid = os.fork()
        if pid > 0:
            pids.append(pid)
        else:
            s = slice(mink, maxk)  # 子程式開始計算
            client = redis.StrictRedis()
            client.rpush("result", str(s))  # 傳遞子程式結果
            sys.exit(0)  # 子程式結束
    for pid in pids:
        os.waitpid(pid, 0)  # 等待子程式結束
    sum = 0
    client = redis.StrictRedis()
    for s in client.lrange("result", 0, -1):
        sum += float(s)  # 收集子程式計算結果
    return math.sqrt(sum * 8)


print pi(10000000)
複製程式碼

我們將級數之和的計算拆分成10個子程式計算,每個子程式負責1/10的計算量,並將計算的中間結果扔到redis的佇列中,然後父程式等待所有子程式結束,再將佇列中的資料全部彙總起來計算最終結果。

輸出如下

3.14159262176
複製程式碼

這個結果和單程式結果一致,但是花費的時間要縮短了不少。

這裡我們之所以使用redis作為程式間通訊方式,是因為程式間通訊是一個比較複雜的技術,我們需要單獨一篇文章來仔細講,各位讀者請耐心聽我下回分解,我們將會使用程式間通訊技術來替換掉這裡的redis。

Python多程式程式設計基礎——圖文版

閱讀python相關高階文章,請關注公眾號「碼洞」

相關文章