進程(process)是正在運行的程序的實例,但一個程序可能會產(chǎn)生多個進程。比如,打開 Chrome 瀏覽器程序,它可能會產(chǎn)生多個進程,主程序需要一個進程,一個網(wǎng)頁標簽需要一個進程,一個插件也需要一個進程,等等。
每個進程都有自己的地址空間,內(nèi)存,數(shù)據(jù)棧以及其他記錄其運行狀態(tài)的輔助數(shù)據(jù),不同的進程只能使用消息隊列、共享內(nèi)存等進程間通訊(IPC)方法進行通信,而不能直接共享信息。
在介紹 Python 的進程編程之前,讓我們先看看 Unix/Linux 中的 fork 函數(shù)。在 Unix/Linux 系統(tǒng)中,fork 函數(shù)被用于創(chuàng)建進程。這個函數(shù)很特殊,對于普通的函數(shù),調(diào)用它一次,返回一次,但是調(diào)用 fork 一次,它返回兩次。事實上,fork 函數(shù)創(chuàng)建了新的進程,我們把它稱為子進程,子進程幾乎是當前進程(即父進程)的一個拷貝:它會復制父進程的代碼段,堆棧段和數(shù)據(jù)段。
對于父進程,fork 函數(shù)返回了子進程的進程號 pid,對于子進程,fork 函數(shù)則返回 0,這也是 fork 函數(shù)返回兩次的原因,根據(jù)返回值,我們可以判斷進程是父進程還是子進程。
下面我們看一段 C 代碼,它展示了 fork 的基本使用:
#include <unistd.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
int pid;
pid = fork(); // 使用 fork 函數(shù)
if (pid < 0) {
printf("Fail to create process\n");
}
else if (pid == 0) {
printf("I am child process (%d) and my parent is (%d)\n", getpid(), getppid());
}
else {
printf("I (%d) just created a child process (%d)\n", getpid(), pid);
}
return 0;
}
其中,getpid 用于獲取當前進程號,getppid 用于獲取父進程號。
事實上,Python 的 os 模塊包含了普遍的操作系統(tǒng)功能,該模塊也提供了 fork 函數(shù),把上面的代碼改成用 Python 來實現(xiàn),如下:
import os
pid = os.fork()
if pid < 0:
print 'Fail to create process'
elif pid == 0:
print 'I am child process (%s) and my parent is (%s).' % (os.getpid(), os.getppid())
else:
print 'I (%s) just created a child process (%s).' % (os.getpid(), pid)
運行上面的代碼,產(chǎn)生如下輸出:
I (86645) just created a child process (86646).
I am child process (86646) and my parent is (86645).
需要注意的是,雖然子進程復制了父進程的代碼段和數(shù)據(jù)段等,但是一旦子進程開始運行,子進程和父進程就是相互獨立的,它們之間不再共享任何數(shù)據(jù)。
Python 提供了一個 multiprocessing 模塊,利用它,我們可以來編寫跨平臺的多進程程序,但需要注意的是 multiprocessing 在 Windows 和 Linux 平臺的不一致性:一樣的代碼在 Windows 和 Linux 下運行的結(jié)果可能不同。因為 Windows 的進程模型和 Linux 不一樣,Windows 下沒有 fork。
我們先來看一個簡單的例子,該例子演示了在主進程中啟動一個子進程,并等待其結(jié)束,代碼如下:
import os
from multiprocessing import Process
# 子進程要執(zhí)行的代碼
def child_proc(name):
print 'Run child process %s (%s)...' % (name, os.getpid())
if __name__ == '__main__':
print 'Parent process %s.' % os.getpid()
p = Process(target=child_proc, args=('test',))
print 'Process will start.'
p.start()
p.join()
print 'Process end.'
在上面的代碼中,我們從 multiprocessing 模塊引入了 Process,Process 是一個用于創(chuàng)建進程對象的類,其中,target 指定了進程要執(zhí)行的函數(shù),args 指定了參數(shù)。在創(chuàng)建了進程實例 p 之后,我們調(diào)用 start 方法開始執(zhí)行該子進程,接著,我們又調(diào)用了 join 方法,該方法用于阻塞子進程以外的所有進程(這里指父進程),當子進程執(zhí)行完畢后,父進程才會繼續(xù)執(zhí)行,它通常用于進程間的同步。
可以看到,用上面這種方式來創(chuàng)建進程比直接使用 fork 更簡單易懂。現(xiàn)在,讓我們看下輸出結(jié)果:
Parent process 7170.
Process will start.
Run child process test (10075)...
Process end.
import random
import os
from multiprocessing import Process
num = random.randint(0, 100)
def show_num():
print("pid:{}, num is {}".format(os.getpid(), num))
if __name__ == "__main__":
print("pid:{}, num is {}".format(os.getpid(), num))
p = Process(target=show_num)
p.start()
p.join()
在 Windows 下運行以上代碼,輸出的結(jié)果如下(你得到不一樣的結(jié)果也是對的):
pid:6504, num is 25
pid:6880, num is 6
我們發(fā)現(xiàn),num 的值是不一樣的!
在 Linux 下運行以上代碼,可以看到 num 的值是一樣的:
pid:11747, num is 13
pid:11748, num is 13
在上面,我們只是創(chuàng)建了一個進程,如果要創(chuàng)建多個進程呢?Python 提供了進程池的方式,讓我們批量創(chuàng)建子進程,讓我們看一個簡單的示例:
import os, time
from multiprocessing import Pool
def foo(x):
print 'Run task %s (pid:%s)...' % (x, os.getpid())
time.sleep(2)
print 'Task %s result is: %s' % (x, x * x)
if __name__ == '__main__':
print 'Parent process %s.' % os.getpid()
p = Pool(4) # 設置進程數(shù)
for i in range(5):
p.apply_async(foo, args=(i,)) # 設置每個進程要執(zhí)行的函數(shù)和參數(shù)
print 'Waiting for all subprocesses done...'
p.close()
p.join()
print 'All subprocesses done.'
在上面的代碼中,Pool 用于生成進程池,對 Pool 對象調(diào)用 apply_async 方法可以使每個進程異步執(zhí)行任務,也就說不用等上一個任務執(zhí)行完才執(zhí)行下一個任務,close 方法用于關(guān)閉進程池,確保沒有新的進程加入,join 方法會等待所有子進程執(zhí)行完畢。
看看執(zhí)行結(jié)果:
Parent process 7170.
Run task 1 (pid:10320)...
Run task 0 (pid:10319)...
Run task 3 (pid:10322)...
Run task 2 (pid:10321)...
Waiting for all subprocesses done...
Task 1 result is: 1
Task 0 result is: 0
Run task 4 (pid:10320)...
Task 3 result is: 9
Task 2 result is: 4
Task 4 result is: 16
All subprocesses done.
進程間的通信可以通過管道(Pipe),隊列(Queue)等多種方式來實現(xiàn)。Python 的 multiprocessing 模塊封裝了底層的實現(xiàn)機制,讓我們可以很容易地實現(xiàn)進程間的通信。
下面以隊列(Queue)為例,在父進程中創(chuàng)建兩個子進程,一個往隊列寫數(shù)據(jù),一個從對列讀數(shù)據(jù),代碼如下:
# -*- coding: utf-8 -*-
from multiprocessing import Process, Queue
# 向隊列中寫入數(shù)據(jù)
def write_task(q):
try:
n = 1
while n < 5:
print "write, %d" % n
q.put(n)
time.sleep(1)
n += 1
except BaseException:
print "write_task error"
finally:
print "write_task end"
# 從隊列讀取數(shù)據(jù)
def read_task(q):
try:
n = 1
while n < 5:
print "read, %d" % q.get()
time.sleep(1)
n += 1
except BaseException:
print "read_task error"
finally:
print "read_task end"
if __name__ == "__main__":
q = Queue() # 父進程創(chuàng)建Queue,并傳給各個子進程
pw = Process(target=write_task, args=(q,))
pr = Process(target=read_task, args=(q,))
pw.start() # 啟動子進程 pw,寫入
pr.start() # 啟動子進程 pr,讀取
pw.join() # 等待 pw 結(jié)束
pr.join() # 等待 pr 結(jié)束
print "DONE"
執(zhí)行結(jié)果如下:
write, 1
read, 1
write, 2
read, 2
write, 3
read, 3
write, 4
read, 4
write_task end
read_task end
DONE