如何优雅地实现Python通用多线程/进程并行模块
当单线程性能不足时,我们通常会使用多线程/多进程去加速运行。而这些代码往往多得令人绝望,需要考虑:
- 如何创建线程执行的函数?
- 如何收集结果?若希望结果从子线程返回主线程,则还要使用队列
- 如何取消执行? 直接kill掉所有线程?信号如何传递?
- 是否需要线程池? 否则反复创建线程的成本过高了
不仅如此,若改为多进程或协程,代码还要继续修改。若多处使用并行,则这些代码还会重复很多遍,非常痛苦。
于是,我们考虑将并行的所有逻辑封装到一个模块之内,向外部提供像串行执行一样的编程体验,还能彻底解决上面所述的疑难问题。所有代码不足180行。
GitHub地址:
使用时非常简洁:
def xprint(x):
time.sleep(1) # mock a long time task
yield x*x
i=0
for item in multi_yield(xrange(100)),xprint, process_mode,3:
i+=1
print(item)
if i>10:
break
上面的代码会使用三个进程,并行地打印1-10的平方。当打印完10之后,进程自动回收释放。就像串行程序一样简单。
1. 先实现串行任务
我们通常会将任务分割为很多个子块,从而方便并行。因此可以将任务抽象为生成器。类似下面的操作,每个seed都是任务的种子。
def get_generator():
for seed in 100:
yield seed
任务本身的定义,则可以通过一个接受种子的函数来实现:
def worker(seed):
# some long time task
return seed*seed # just example
那么实现串行任务就像这样:
for seed in get_generator(n):
print worker(seed)
进一步地,可以将其抽象为下面的函数:
def serial_yield(genenator,worker):
for seed in generator():
yield worker(seed)
该函数通过传入生成器函数(generator)和任务的定义(worker函数),即可再返回一个生成器。消费时:
for result in serial_yield(your_genenator, your_worker):
print(result)
我们看到,通过定义高阶函数,serial_yield就像map函数,对seed进行加工后输出。
2. 定义并行任务
考虑如下场景: boss负责分发任务到任务队列,多个worker从任务队列捞数据,处理完之后,再写入结果队列。主线程从结果队列中取结果即可。
我们定义如下几种执行模式:
- async: 异步/多协程
- thread: 多线程
- process: 多进程
使用Python创建worker的代码如下,func是任务的定义(是个函数)
def factory(func, args=None, name='task'):
if args is None:
args = ()
if mode == process_mode:
return multiprocessing.Process(name=name, target=func, args=args)
if mode == thread_mode:
import threading
t = threading.Thread(name=name, target=func, args=args)
t.daemon = True
return t
if mode == async_mode:
import gevent
return gevent.spawn(func, *args)
创建队列的代码如下,注意seeds可能是无穷流,因此需要限定队列的长度,当入队列发现队列已满时,则任务需要阻塞。
def queue_factory(size):
if mode == process_mode:
return multiprocessing.Queue(size)
elif mode == thread_mode:
return Queue(size)
elif mode == async_mode:
from gevent import queue
return queue.Queue(size)
什么时候任务可以终止? 我们罗列如下几种情况:
- 所有的seed都已经被消费完了
- 外部传入了结束请求
对第一种情况,我们让boss在seed消费完之后,在队列里放入多个Empty标志,worker收到Empty之后,就会自动退出,下面是boss的实现逻辑:
def _boss(task_generator, task_queue, worker_count):
for task in task_generator:
task_queue.put(task)
for i in range(worker_count):
task_queue.put(Empty)
print('worker boss finished')
再定义worker的逻辑:
def _worker(task_queue, result_queue, gene_func):
import time
try:
while not stop_wrapper.is_stop():
if task_queue.empty():
time.sleep(0.01)
continue
task = task.get()
if task == Empty:
result_queue.put(Empty)
break
if task == Stop:
break
for item in gene_func(task):
result_queue.put(item)
print ('worker worker is stop')
except Exception as e:
logging.exception(e)
print ('worker exception, quit')
简单吧?但是这样会有问题,这个后面再说,我们把剩余的代码写完。
再定义multi_yield的主要代码。 代码非常好理解,创建任务和结果队列,再创建boss和worker线程(或进程/协程)并启动,之后不停地从结果队列里取数据就可以了。
def multi_yield(customer_func, mode=thread_mode, worker_count=1, generator=None, queue_size=10):
workers = []
result_queue = queue_factory(queue_size)
task_queue = queue_factory(queue_size)
main = factory(_boss, args=(generator, task_queue, worker_count), name='_boss')
for process_id in range(0, worker_count):
name = 'worker_%s' % (process_id)
p = factory(_worker, args=(task_queue, result_queue, customer_func), name=name)
workers.append(p)
main.start()
for r in workers:
r.start()
count = 0
while not should_stop():
data = result_queue.get()
if data is Empty:
count += 1
if count == worker_count:
break
continue
if data is Stop:
break
else:
yield data
这样从外部消费时,即可:
def xprint(x):
time.sleep(1)
yield x
i=0
for item in multi_yield(xprint, process_mode,3,xrange(100)):
i+=1
print(item)
if i>10:
break
这样我们就实现了一个与serial_yield功能类似的multi_yield。可以定义多个worker,从队列中领任务,而不需重复地创建和销毁,更不需要线程池。当然,代码不完全,运行时可能出问题。但以上代码已经说明了核心的功能。完整的代码可以在文末找到。
但是你也会发现很严重的问题:
- 当从外部break时,内部的线程并不会自动停止
- 我们无法判断队列的长度,若队列满,那么put操作会永远卡死在那里,任务都不会结束。
3. 改进任务停止逻辑
最开始想到的,是通过在multi_yield函数参数中添加一个返回bool的函数,这样当外部break时,同时将该函数的返回值置为True,内部检测到该标志位后强制退出。伪代码如下:
_stop=False
def can_stop():
return _stop
for item in multi_yield(xprint, process_mode,3,xrange(100),can_stop):
i+=1
print(item)
if i>10:
_stop=True
break
但这样并不优雅,引入了更多的函数作为参数,还必须手工控制变量值,非常繁琐。在多进程模式下,stop标志位还如何解决?
我们希望外部在循环时执行了break后,会自动通知内部的生成器。实现方法似乎就是with语句,即contextmanager.
我们实现以下的包装类:
class Yielder(object):
def __init__(self, dispose):
self.dispose = dispose
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
self.dispose()
它实现了with的原语,参数是dispose函数,作用是退出with代码块后的回收逻辑。
由于值类型的标志位无法在多进程环境中传递,我们再创建StopWrapper类,用于管理停止标志和回收资源:
class Stop_Wrapper():
def __init__(self):
self.stop_flag = False
self.workers=[]
def is_stop(self):
return self.stop_flag
def stop(self):
self.stop_flag = True
for process in self.workers:
if isinstance(process,multiprocessing.Process):
process.terminate()
最后的问题是,如何解决队列满或空时,put/get的无限等待问题呢?考虑包装一下put/get:包装在while True之中,每隔两秒get/put,这样即使阻塞时,也能保证可以检查退出标志位。所有线程在主线程结束后,最迟也能在2s内自动退出。
def safe_queue_get(queue, is_stop_func=None, timeout=2):
while True:
if is_stop_func is not None and is_stop_func():
return Stop
try:
data = queue.get(timeout=timeout)
return data
except:
continue
def safe_queue_put(queue, item, is_stop_func=None, timeout=2):
while True:
if is_stop_func is not None and is_stop_func():
return Stop
try:
queue.put(item, timeout=timeout)
return item
except:
continue
如何使用呢?我们只需在multi_yield的yield语句之外加上一行就可以了:
with Yielder(stop_wrapper.stop):
# create queue,boss,worker, then start all
# ignore repeat code
while not should_stop():
data = safe_queue_get(result_queue, should_stop)
if data is Empty:
count += 1
if count == worker_count:
break
continue
if data is Stop:
break
else:
yield data
仔细阅读上面的代码, 外部循环时退出循环,则会自动触发stop_wrapper的stop操作,回收全部资源,而不需通过外部的标志位传递!这样调用方在心智完全不需有额外的负担。
实现生成器和上下文管理器的编程语言,都可以通过上述方式实现自动协程资源回收。笔者也实现了一个C#版本的,有兴趣欢迎交流。
这样,我们就能像文章开头那样,实现并行的迭代器操作了。
4. 结语
完整代码在:
https://github.com/ferventdesert/multi_yielder/blob/master/src/multi_yielder.py
一些实现的细节很有趣,我们借助在函数中定义函数,可以不用复杂的类去承担职责,而仅仅只需函数。而类似的思想,在函数式编程中非常常见。
该工具已经被笔者的流式语言etlpy所集成。但是依然有较多改进的空间,如没有集成分布式执行模式。
欢迎留言交流。
如何优雅地实现Python通用多线程/进程并行模块的更多相关文章
- Python之多线程:Threading模块
1.Threading模块提供的类 Thread,Lock,Rlock,Condition,Semaphore,Event,Timer,local 2.threading模块提供的常用的方法 (1)t ...
- 优雅地记录Python程序日志2:模块组件化日志记录器
本文摘自:https://zhuanlan.zhihu.com/p/32043593 本篇将会涉及: logging的各个模块化组件 构建一个组件化的日志器 logging的模块组件化 在上一篇文章中 ...
- Python 多线程进程高级指南(二)
本文是如何<优雅地实现Python通用多线程/进程并行模块>的后续.因为我发现,自认为懂了一点多线程开发的皮毛,写了那么个multi_helper的玩意儿,后来才发现我靠原来就是一坨屎.自 ...
- 【转】使用python进行多线程编程
1. python对多线程的支持 1)虚拟机层面 Python虚拟机使用GIL(Global Interpreter Lock,全局解释器锁)来互斥线程对共享资源的访问,暂时无法利用多处理器的优势.使 ...
- Python多线程&进程
一.线程&进程 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程, ...
- Python 浅析线程(threading模块)和进程(process)
线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 进程与线程 什么 ...
- Python的多线程和多进程
(1)多线程的产生并不是因为发明了多核CPU甚至现在有多个CPU+多核的硬件,也不是因为多线程CPU运行效率比单线程高.单从CPU的运行效率上考虑,单任务进程及单线程效率是最高的,因为CPU没有任何进 ...
- python 异常处理、进程
目录: 异常处理 python进程 python并发之多进程 一.异常处理(try...except...) 1.程序中难免出现错误,而错误分成两种: a.语法错误: b.逻辑错误(逻辑错误) 2.异 ...
- Python 中的进程与 锁
理论知识 操作系统背景知识 顾名思义,进程即正在执行的一个过程.进程是对正在运行程序的一个抽象. 进程的概念起源于操作系统,是操作系统最核心的概念,也是操作系统提供的最古老也是最重要的抽象概念之一.操 ...
随机推荐
- WebGIS中使用ZRender实现前端动态播放轨迹特效的方案
文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 项目中需要在地图上以时间轴方式播放人员.车辆在地图上的历史行进 ...
- Linux - 进程调度算法
进程调度: 无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数.这将导致它们互相争夺处理机.另外,系统进程也同样需要使用处理机. 这就要求进程调度程序按一定的策略,动态地把处理机分配给处于 ...
- python基本数据类型——tuple
一.元组的创建与转换: ages = (11, 22, 33, 44, 55) ages = tuple((11, 22, 33, 44, 55))ages = tuple([]) # 字符串.列表. ...
- 学习Java之前操作环境的安装及配置
1.根据自己的系统版本下载相应版本的JDK(Java开发运行时环境) 查看自己系统版本的方法:在桌面上右键计算机(win7,win10是此电脑,XP是我的电脑),点击属性,进入到计算机属性页面以后里面 ...
- MySQL之使用DDL语句创建表
一.使用DDL语句创建表 DDL语言全面数据定义语言(Data Define Language) 主要的DDL动词: CREATE(创建).DROP(删除).ALTER(修改) TRUNCATE(截断 ...
- linq语句复杂查询和分开查询的性能对比
刚开始以为复杂的linq语句查询会不会比分开来写效率高,因为复杂的语句关联和嵌套多,执行应该慢.分开写虽然多了一次io处理,但是关联比较少,数据了比价少,和朋友讨了一下,回家就做了个测试,废话不多说, ...
- 结束C#2的讲解:最后的一些特性
分部类型 可以在多个源文件中为一个类型编写代码.特别适合用于部分代码是自动生成,而其他部分的代码为手动类型. 多个源代码文件组成的类型为分部类型 #region 7-1演示分部类型的混合声明 part ...
- 移动端 H5图片裁剪插件,内置简单手势操作
前面曾经写过一篇<H5图片裁剪升级版>,但里面需要借助第三方手势库,这次就不需要使用手势库,全部封装在代码中. 下图是裁剪的展示,下面就做了拖放和裁剪,没有做缩放,在插件中需要用到大量的计 ...
- [Splay模版1]
输入 第1行:1个正整数n,表示操作数量,100≤n≤200,000 第2..n+1行:可能包含下面3种规则: 1个字母'I',紧接着1个数字k,表示插入一个数字k到树中,1≤k≤1,000,000, ...
- java web (j2ee)学习路线 —— 将青春交给命运
RESON TO DO JAVA WEB:1.JAVA WEB(企业级) 2.Android和iOS过于火爆并且不兼容 一.JAVA WEB开发需要的知识储备 1. 基本的网页设计语言:H ...