Python multiprocessing 模块解析 (1) – context

前言:
工作中不少场景是开发后台脚本独, 比如定时job, 扫表, 事件消费等任务.
Python 是首选语言, 其次才是编译型的Golang(有性能要求的任务). 多数
时候单进程脚本足够执行任务, 少数如高频的UGC社区顶踩动作, api 层
将动作写入消息队列异步处理(mq-> job平台 or 自管理的消费脚本进程),
单进程脚本消费能力跟不上, 如果考虑坚持用 Python 情况下同时部署
多个脚本进程消费或者基于 multiprocessing 都是合适的解决方案. 这里
就引出了 multiprocessing 模块, 既然与该模块常打交道, 那么可以带着
好奇心去看看模块的实现 :) 本文以 Python3.5探索其机制

本文就以 context 为起点开始模块探索.

为什么以 context 开始?

因为 __init__.py 源码是以子模块 context 来辅助. 见源码:

init 入口代码非常简洁, 通常我们使用 from multiprocessing import xxx, 然而这个
xxx 并没有在 init 文件中显示引出来, 而是通过子模块 context 来引出模块成员, 将
context 里的内容更新到根级的 globals() 字典中, 同时 __all__ 列表对外暴露的成员
范围也作了限制. 由此看来, context 的命名也名副其实, 所有成员在其上下文内. 探索
multiprocessing 自然从 context 入手.

context源码中 的 _default_context 其实是 DefaultContext 实例, 同时继承了
BaseContext 成员.实际上 from multiprocessing import xxx 即引出的是
DefaultContext实例拥有的 public 属性, 包括函数.

context 会根据自身运行环境的系统平台选择对应子进程的创建方式, 并以具体命名的
上下文来表示, eg: ForkContext 类, 该类是 Linux平台中, DefaultContext 实例创建时
默认传入的参数.

阅读源码中会发现, ForkProcess 的 Popen 函数, 以及 DefaultContext 自身拥有的
成员函数以驼峰命名, 并且 from multiprocessing import Xyy 时, 实际上就是刚才
提到的成员, Xyy 作为代理方法, 返回真正的对应含义的实例.
例如样例代码:
from multiprocessing import Manager
实际是调用context实例的 Manager 方法, 函数内部在真正引入 manager 模块,
返回真正的 multiprocessing.SyncManager 实例. 这里引出的疑问是:

context 作为代理, 而不显示使用引出子模块的设计初衷是什么?

首先是引出一个重要的线索: What’s New In Python 3.4
mp 模块的 context 机制是在Py3.4版本引入的, 在Py2和早期Py3版本里, 并没有
context的概念. 将原本在 init 里定义的类和函数迁移到 context 里, 目的是解决
Windows 和 Linux 创建进程时的结果数据不一致的问题. 原先的 mp 模块 fork
是可以在任何平台直接使用的(因为是显示的代码), 该方法在 Linux 中使用正常,
而 win 如果直接调用, 会有非预期的效果, 因为 fork 并不在 win下支持. 在3.4
之后使用 context 的概念将创建进程的方法隐藏, 对使用者是透明的. init 文件的
__all__ 字典最终暴露对外的模块是 context 里类和方法, 根据平台不同,内部实现
的代码是不同的. 同时 context 里 提供的 set_start_method 可以选择平台支持的
进程创建方法, 比如 Linux 支持 spawn fork forkserver, 而 win 平台只支持 spawn.

提供给调用者的类和函数是 multiprocessing::context::DefaultContext 实例
_default_context 的成员函数, DefaultContext 继承 BaseContext, 该子模块
由两个 context 来主持大局:
* _default_context 对外暴露成员方法的它的职责相当于代理, 
根据系统平台对应的进程行为则由该实例成员 _default_context 负责
* _concrete_contexts 它是一个字典, DefaultContext实例的会根据平台选择默认类型对应的 context 作为其成员.

这两个实例从字面理解非常绕, 以图形方式来解释关系:

关于 context 引入更多的历史信息, 可以参考来自 Python bug tracker 的资料, 建议
同时阅读源和调试源码, 这样可方便摸清其设计思路:
issue8713 – multiprocessing needs option to eschew fork() under Linux
issue18999 – Support different contexts in multiprocessing

小结:
* context 的引入有效解决了 mp 模块在不同系统中, 只暴露有用的类和方法.
* 在探究源码设计背后的思路时, 了解代码演进的历史是一个有效的方式.

原创文章,转载请注明: 转载自kaka_ace's blog

本文链接地址: Python multiprocessing 模块解析 (1) – context

发表评论

电子邮件地址不会被公开。 必填项已用*标注