程序员

【Python入门】21.进程编程之 multiprocessi

2018-08-20  本文已影响140人  三贝_

摘要:多进程编程;fork( )的介绍;multiprocess模块;Process类;Pool类;subprocess类;进程间通信;Queue队列;分布式进程


*写在前面:为了更好的学习python,博主记录下自己的学习路程。本学习笔记基于廖雪峰的Python教程,如有侵权,请告知删除。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ *


目录

进程和线程
多进程
fork( )
multiprocessing
• Process类
• Process类的常用方法
• Pool类
• Pool类的常用方法
subprocess
• subprocess.Popen类
进程间通信
• Queue的定义
• Queue的常用方法
实现分布式进程

进程和线程

多任务:现在的操作系统都是支持“多任务”的,如Windows、Mac OS X等。“多任务”是指可以同时运行多个任务如一边听歌一边浏览网页、一边玩游戏一边听歌。

实际上,“多任务”的执行不是同时进行的,而是多个任务交替执行的。如任务1执行0.01秒,然后任务2执行0.01秒,又回到任务1执行0.01秒。

即使是多核,任务数也往往多于CPU核心数,多个任务会在单个核心上交替执行。

进程:对于操作系统而言,一个任务就是一个进程,比如我们打开了一个浏览器,就是启动了一个浏览器进程。

线程:一个进程的内部也会有多个任务,如在浏览器中我们可以打字、可以看视频等,我们称这些子任务为线程。

线程是最小的执行单位,一个进程至少包含一个线程。

要想实现“多任务”,有三种方式:

1.多进程:即启动多个进程,每个进程包含一个线程;
2.多线程,即启动一个进程,这个进程包含多个线程;
3.多进程+多线程,即启动多个进程,每个进程包含多个线程。

“多任务”的编写具有一定的复杂性,但有时候我们不得不执行多任务,如在看电影时要看视频的同时还要音频。

Python既支持多进程,也支持多线程。

多进程

fork( )

Unix/Linux操作系统提供了一个fork()系统调用,而Windows系统没有fork( )调用。

调用fork( )能够自动使父进程生成一个子进程,并且在分别在父进程与子进程内返回一次。

子进程返回的是0,而父进程返回的是子进程的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.

其中getpid( )方法可以拿到当前进程的ID,子进程用getppid( )方法可以拿到其父进程的ID。

分析一下上面代码的运行过程:

首先,引入模块os,执行print语句。
然后,调用fork( ),fork( )首先返回的是子进程的ID,则pid不为0,执行else后的缩进语句。
其次,fork( ) 第二次返回值是0,则pid为0,执行if后的缩进语句。

multiprocessing

Python是一个跨平台的多进程支持,虽然Windows上无法调用fork( ),但是Python中还有其他模块,如multiprocessing模块。

multiprocessing模块就是跨平台版本的多进程模块。它里面提供了一个Process类来表示进程对象。

• Process类

创建Process类的实例构造:

def __init__(self, group=None, target=None, name=None, args=(), kwargs={})

参数说明:
group:进程所属组(基本不用)
target:进程调用对象
name:进程别名
args:调用对象的位置参数
kwargs:调用对象的关键字参数

• Process类的常用方法

start( ):启动进程。
join([timeout]):暂停运行后面代码,知道调用此方法的进程执行完毕或到达指定timeout
terminate( ):立即停止该进程
is_live( ):返回该进程是否在运行

一个用Process创建子进程的简单例子(除注释外代码转自廖雪峰的官方网站)

from multiprocessing import Process                        #引入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.
• Pool类

Pool,进程池。如果我们想要批量创建子进程,就要用到multiprocessing模块中的Pool类。

• Pool类的常用方法:

1.apply( ):与apply( )函数一致,主进程会被阻塞直到函数执行结束;
2.apply_async( ):与apply( )方法一样,但它是非阻塞的且支持结果返回后进行回调;
3.map( ):与map( )函数一致,主进程会被阻塞直到函数执行结束;
4.map_async( ):与map( )方法一样,但它是非阻塞的;
5.close( ):关闭进程池,不再接受新的任务;
6.terminal( ):结果进程,不再处理未处理的任务;
7.join( ):父进程阻塞,等待子进程执行完毕,在close( )和terminal( )后使用。

(apply( )函数:apply(func[, args=()[, kwds={}]]),把位置参数与关键字参数传入func中,并返回值)
(map( )函数:map(func, iterable[, chunksize=None]),把可迭代对象依次传入func中,并返回值)

看一个批量生成子进程的例子(除注释外代码转自廖雪峰的官方网站)

from multiprocessing import Pool                             #从multiprocessing模块中引入Pool
import os, time, random                                      #引入os,time,random模块

def long_time_task(name):                                    #定义一个子进程调用的函数
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()                                      #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):                                       #依次取i从0到4
        p.apply_async(long_time_task, args=(i,))             #每个进程调用指定的函数对象,即生成了5个进程
    print('Waiting for all subprocesses done...')
    p.close()                                                #不再接受新的任务
    p.join()                                                 #等待子进程执行完毕
    print('All subprocesses done.')

运行结果:

Parent process 15088.
Waiting for all subprocesses done...
Run task 0 (8516)...
Run task 1 (9308)...
Run task 2 (9092)...
Run task 3 (15112)...
Task 0 runs 1.29 seconds.
Run task 4 (8516)...
Task 4 runs 0.41 seconds.
Task 1 runs 2.08 seconds.
Task 2 runs 2.31 seconds.
Task 3 runs 2.38 seconds.
All subprocesses done.

代码解析:

在父进程执行到p.close( )及p.join( )后暂停执行,等待子进程执行完毕。

由于我们设定了p = Pool(4),即进程池里最多有四个进程,换而言之,就是最多同时执行4个进程。所以我们留意到在执行完子进程task1、2、3、4之后,需要等待其中的一个子进程执行完毕,才能继续执行task4。

如果我们改成p = Pool(5)的话,就可以同时执行5个进程了:

Parent process 11480.
Waiting for all subprocesses done...
Run task 0 (2844)...
Run task 1 (7948)...
Run task 2 (14772)...
Run task 3 (5404)...
Run task 4 (9236)...
Task 3 runs 0.39 seconds.
Task 1 runs 0.62 seconds.
Task 4 runs 0.73 seconds.
Task 2 runs 1.67 seconds.
Task 0 runs 2.71 seconds.
All subprocesses done.

subprocess

很多时候,我们并不是要自己创建子进程,而是需要启动外部进程。

subprocess模块可以让我们启动一个子进程,并且能够控制其输入与输出。

我们可以通过subprocess.call()函数来执行子进程。比如我们要运行命令nslookup www.python.org(其中nslookup是一个域名查询命令):

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

这与在命令行模式中直接输入nslookup www.python.org,运行的结果一致。

• subprocess.Popen类

如果还需要进行输入输出,那么就要用到subprocess.Popen类。

事实上,subprocess模块的函数都是基于subprocess.Popen类实现的。如果想要实现一些不太常见的功能时,就要通过subprocess.Popen类提供的api来实现了。

subprocess.Popen类的函数构造:

class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, 
    preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False,
    startup_info=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())

常用参数说明:

args:即要执行的shell命令,可以是字符串,也可以是各命令参数组成的序列,一般为序列;
stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。
shell: 该参数用于标识是否使用shell作为要执行的程序,默认为False,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
(shell:俗称壳,区别于核,是指提供使用者使用界面的软件,就是命令解析器,比如Windows下的cmd)

其他参数说明(转自云游道士,原文请看这里):

bufsize: 指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。

preexec_fn: 用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。

close_fds: 如果该参数的值为True,则除了0,1和2之外的所有文件描述符都将会在子进程执行之前被关闭。

cwd: 如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。

env: 用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。

universal_newlines: 如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。

startupinfo和creationflags: 这两个参数只在Windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。

subprocess.Popen类的常用方法(转自云游道士,原文请看这里):

Popen.poll() :用于检查子进程(命令)是否已经执行结束,没结束返回None,结束后返回状态码。

Popen.wait(timeout=None):等待子进程结束,并返回状态码;如果在timeout指定的秒数之后进程还没有结束,将会抛出一个TimeoutExpired异常。

Popen.communicate(input=None, timeout=None):该方法可用来与进程进行交互,比如发送数据到stdin,从stdout和stderr读取数据,直到到达文件末尾。

Popen.send_signal(signal):发送指定的信号给这个子进程。

Popen.terminate():停止该子进程。

Popen.kill():杀死该子进程。

我们通过Popen.communicate( )方法就可以对子进程进行输入了:

import subprocess

print('$ nslookup')

p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
#把需要输入的内容传给Output,err为None
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

进程间通信

操作系统提供了很多机制来实现进程间的通信,在Python中,可以用Queue、Pipes等来实现数据交换。
这里用Queue作为例子,先补充介绍Queue:

• Queue的定义
q = Queue(maxsize = x)

定义一个长度为x的队列,当maxsize为默认值或小于1时,队列为无限长度。

• Queue的常用方法

q.put( ):放入元素
q.get( ):获取元素
q.qsize( ):返回队列大小
q.empty( ):若队列为空,返回True,反之返回False
q.full( ):若队列满了,返回True,反之返回False

看一个简单的例子(代码转自廖雪峰的官方网站)

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.

实现分布式进程

Python中的multiprocessing模块不但支持多进程,其managers子模块还支持把多进程分布到多台机器上。

如在上例中,通过Queue实现了多进程间的通信,现在由于任务繁重,我们希望把发送任务的进程与执行任务的进程分布在两个机器上工作,要怎么实现呢?

就要通过managers模块把Queue通过网络暴露出去,让其他机器的进程访问Queue。

首先我们要在主机上编写服务进程。服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:
(以下例子转自廖雪峰官网,由于原码在Windows下运行有错误,所以略有修改,使得可以在Windows运行)

首先编写服务进程task_master.py:

#task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()


def return_task_queue():
    global task_queue
    return task_queue

def return_result_queue():
    global result_queue
    return result_queue

# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
    pass

#加入main判断
if __name__ == '__main__':

# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
    QueueManager.register('get_task_queue',callable=return_task_queue)
    QueueManager.register('get_result_queue',callable=return_result_queue)
    
# 绑定端口34512, 设置验证码'abc':
    manager = QueueManager(address=('127.0.0.1',34512), authkey=b'abc')
# 启动Queue:
    manager.start()
# 获得通过网络访问的Queue对象:
    task = manager.get_task_queue()
    result = manager.get_result_queue()
# 放几个任务进去:
    for i in range(10):
        n = random.randint(0, 10000)
        print('Put task %d...' % n)
        task.put(n)
# 从result队列读取结果:
    print('Try get result...')
    for i in range(10):
        r = result.get(timeout=10)
        print('Result: %s' %r)
# 关闭:
    manager.shutdown()
    print('master exit.')

之后再另一台机器编写任务进程task_worker.py(本机也可以):

# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 创建类似的QueueManager:
class QueueManager(BaseManager):
    pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 34512), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
    try:
        n = task.get(timeout=1)
        print('run task %d * %d...' % (n, n))
        r = '%d * %d = %d' % (n, n, n*n)
        time.sleep(1)
        result.put(r)
    except Queue.Empty:
        print('task queue is empty.')
# 处理结束:
print('worker exit.')

任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。

现在看一下运行结果,先启动服务进程task_master.py

$ python3 task_master.py 
Put task 3411...
Put task 1605...
Put task 1398...
Put task 4729...
Put task 5300...
Put task 7471...
Put task 68...
Put task 4219...
Put task 339...
Put task 7866...
Try get results...

task_master.py进程发送完任务后,再启动task_worker.py进程:

$ python3 task_worker.py
Connect to server 127.0.0.1...
run task 3411 * 3411...
run task 1605 * 1605...
run task 1398 * 1398...
run task 4729 * 4729...
run task 5300 * 5300...
run task 7471 * 7471...
run task 68 * 68...
run task 4219 * 4219...
run task 339 * 339...
run task 7866 * 7866...
worker exit.

启动task_worker.py进程的同时,task_master.py进程会继续打印结果:

Result: 3411 * 3411 = 11634921
Result: 1605 * 1605 = 2576025
Result: 1398 * 1398 = 1954404
Result: 4729 * 4729 = 22363441
Result: 5300 * 5300 = 28090000
Result: 7471 * 7471 = 55815841
Result: 68 * 68 = 4624
Result: 4219 * 4219 = 17799961
Result: 339 * 339 = 114921
Result: 7866 * 7866 = 61873956

这就是一个简单的Master/Worker模型。

需要注意的是,在分布式多进程环境下,Master添加任务到Queue要通过manager.get_task_queue()获得的Queue接口添加,而不能直接对原始的task_queue进行操作。而Worker也是从manager.get_task_queue()获取Queue任务。


以上就是本节的全部内容,感谢你的阅读。

下一节内容:进程编程之 多进程与多线程的比较

有任何问题与想法,欢迎评论与吐槽。

和博主一起学习Python吧( ̄▽ ̄)~*

上一篇下一篇

猜你喜欢

热点阅读