【Python系列专栏】第三十一篇 Python多进程的实现

多进程

fork函数

要让Python程序实现多进程(multiprocessing),我们先得了解操作系统的相关知识。

Unix/Linux操作系统提供了一个 fork() 系统调用函数,它非常特殊。普通的函数在被调用时,调用一次只会返回一次。但是 fork() 函数调用一次会返回两次,因为此时操作系统会自动把当前进程(称为父进程)复制一份(称为子进程),然后分别在父进程和子进程内进行返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用 getppid() 就可以拿到父进程的ID。

Python的 os 模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if 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))

运行结果如下:

Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.

这段代码在执行第一个print时只有一个进程(876),因此只打印一次。然后在执行 fork() 之后,当前进程(876)被复制出一个子进程(877)。当前进程会先返回,返回子进程ID(877),然后往下走进入if-else代码块,进入else语句进行打印;其后子进程(877)返回0,同样往下走,进入if-else代码块,进入if语句进行打印。

由于Windows没有fork调用,上面的代码在Windows上无法运行。而Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的~

有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。


multiprocessing

如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?

由于Python是跨平台的,自然也应该提供跨平台的多进程支持。multiprocessing 模块就是跨平台版本的多进程模块。

multiprocessing 模块提供了一个 Process 类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:

from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')

执行结果如下:

Parent process 928.
Process will start.
Run child process test (929)...
Process end.

创建子进程时,只需要传入一个执行函数和该函数的参数就可以创建一个 Process 实例。用 start() 方法就可以启动这个子进程,这样创建进程要比 fork() 更简单和灵活。

join() 方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。


进程池

如果要启动大量的子进程,我们可以用进程池的方式来批量创建子进程

from multiprocessing import Pool
import os, time, random

# 子进程执行的任务
def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4) # 创建一个大小为4的进程池
    for i in range(5): # 依次创建5个子进程
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

执行结果如下:

Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.

代码解读:

Pool 对象调用 join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close(),调用 close() 之后就不能往进程池中继续添加新的 Process

请注意输出的结果,task 0,1,2,3这四个子进程是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为我们定义进程池时定义了大小为4,也即最多同时执行4个进程,所以第5个进程就要等进程池里有空位了才能开始。如果改成:

p = Pool(5)

就可以同时跑5个进程。

当没有传入参数时,Pool的默认大小是CPU的核数,所以如果电脑是4核CPU则默认进程池大小为4,如果电脑是8核CPU则默认进程池大小为8。


子进程的输入和输出

很多时候,子进程和父进程要执行的不是同一个任务。我们创建了子进程后,还需要控制子进程的输入和输出。

subprocess 模块可以让我们非常方便地启动一个子进程,并且控制其输入和输出。

下面的例子演示了如何在Python代码中运行命令 nslookup www.python.org,这和命令行直接运行的效果是一样的:

import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

运行结果:

$ nslookup www.python.org
Server:        192.168.19.4
Address:    192.168.19.4#53

Non-authoritative answer:
www.python.org    canonical name = python.map.fastly.net.
Name:    python.map.fastly.net
Address: 199.27.79.223

Exit code: 0

看看帮助文档中 subprocess 模块的 call() 函数的描述:

Run command with arguments. Wait for command to complete or timeout, then return the returncode attribute.

它可以帮助我们建立一个子进程来执行系统调用函数,并且允许传入函数,调用后会等待运行结束并最终返回调用函数的返回值,之后才继续执行后续代码。可以只传入一个列表,列表第一个元素为系统调用的名称,第二个元素为参数。

如果子进程执行的过程中还需要其他输入,我们使用 subprocess 模块的 Popen 类初始化子进程,并通过它的 communicate() 方法来实现进程执行过程中的多次输入:

import subprocess

print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n') # 每次输入之间用换行符隔开
print(output.decode('utf-8')) # 返回的是字节流,所以要先解码
print('Exit code:', p.returncode)

上面的代码相当于在命令行执行命令 nslookup,然后手动输入:

set q=mx
python.org
exit

运行结果如下:

$ nslookup
Server:        192.168.19.4
Address:    192.168.19.4#53

Non-authoritative answer:
python.org    mail exchanger = 50 mail.python.org.

Authoritative answers can be found from:
mail.python.org    internet address = 82.94.164.166
mail.python.org    has AAAA address 2001:888:2000:d::a6


Exit code: 0

进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的 multiprocessing 模块包装了底层的机制,提供了 QueuePipes 等多种方式来交换数据。

我们以 Queue 为例,在父进程中创建两个子进程,第一个子进程往 Queue 里写数据,第二个子进程从 Queue 里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

运行结果如下:

Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

在Unix/Linux下,multiprocessing 模块封装了 fork() 调用,使我们不需要关注 fork() 的细节。由于Windows没有fork调用,因此,multiprocessing 需要“模拟”出fork的效果,父进程的所有Python对象都必须先通过 pickle 序列化再传到子进程去。所以,如果 multiprocessing 在Windows下调用失败了,就要先考虑是不是序列化失败了。


小结

在Unix/Linux下,可以使用 fork() 调用实现多进程。

要实现跨平台的多进程,可以使用 multiprocessing 模块。

进程间通信可以通过 QueuePipes 等实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mrrunsen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值