多程式程式設計知識是Python程式設計師進階高階的必備知識點,我們平時習慣了使用multiprocessing庫來操縱多程式,但是並不知道它的具體實現原理。下面我對多程式的常用知識點都簡單列了一遍,使用原生的多程式方法呼叫,幫助讀者理解多程式的實現機制。程式碼跑在linux環境下。沒有linux條件的,可以使用docker或者虛擬機器執行進行體驗。
docker pull python:2.7
複製程式碼
生成子程式
Python生成子程式使用os.fork()
,它將產生一個子程式。fork呼叫同時在父程式和主程式同時返回,在父程式中返回子程式的pid,在子程式中返回0,如果返回值小於零,說明子程式產生失敗,一般是因為作業系統資源不足。
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
是在父程式裡執行,如果是子程式,就不要在呼叫了。
# 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可以使程式休眠任意時間,單位為秒,可以是小數
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)。
# 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函式會返回子程式的退出狀態,它就像子程式留下的臨終遺言,必須等父程式聽到後才能徹底瞑目。
# 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之後,子程式終於停止了輸出。
接下來我們換一個自定義訊號處理函式,子程式收到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。
先使用單程式版本
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
複製程式碼
這個程式跑了有一小會才出結果,不過這個值已經非常接近圓周率了。
接下來我們用多程式版本,我們用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相關高階文章,請關注公眾號「碼洞」