众所周知,python的多线程开发在GIL(全局器解释锁)下饱受诟病,在单核模式下搞多线程对效率的提升相当有限。于是大家的共识就是搞io密集的程序,建议采用多线程,计算密集型的程序就搞多进程。近期的一些开发经历,让我大量尝试采用多进程和异步io的方式来提高效率。
一.采用多进程。
1.用过multiprocessing.process和queue及pool,但是一直有报错,位置在multiprocessing.spawn的_main(fd,parent_sentinel)中reduction.pickle.load(from_parent)。网上提供了各种诸如修改权限,修改reduction协议类型的方案,始终未能解决。最后竟然是更新了python版本后不药而愈,但最后放弃了这种方式。
2.使用concurrent.futures.ProcessPoolExecutor,官方文档很详细。程序运行也很不错,但是在脚本执行完毕后退出时抛出异常。Error in atexit._run_exitfuncs。大致的内容是handle is closed,通过追踪大致可以判断出脚本执行完毕时,会有futures.process的_python_exit()执行,此时ProcessPoolExecutor执行完毕后释放,没有线程可以被wakeup,所以报错。引起这个问题的是官方推荐的with as写法,with as 执行完毕后会将对象释放,结果在退出的时候引发异常。
解决方案也很反常,不使用with as的自动释放,也不使用shutdown手动释放,而是不释放,在整个脚本执行完毕的时候,由_python_exit()进行释放。
有意思的是concurrent.futures.process文档里有这么一段注释,可以自行研究。
# Workers are created as daemon threads and processes. This is done to allow the
# interpreter to exit when there are still idle processes in a
# ProcessPoolExecutor's process pool (i.e. shutdown() was not called). However,
# allowing workers to die with the interpreter has two undesirable properties:
# - The workers would still be running during interpreter shutdown,
# meaning that they would fail in unpredictable ways.
# - The workers could be killed while evaluating a work item, which could
# be bad if the callable being evaluated has external side-effects e.g.
# writing to a file.
#
# To work around this problem, an exit handler is installed which tells the
# workers to exit when their work queues are empty and then waits until the
# threads/processes finish.
二.采用异步io。
python在3.4中有了asyncio。官方给的示例可以看出来这是个消息循环,eventloop是在当前的上下文中提供的,而且可以和协程一起执行。最新的文档里可以看到eventloop已经不再只是同协程一起提供异步了。
我们可以将多个耗时的任务进行封装,丢进eventloop中,当线程执行到需要等待的操作如io,主线程不会等待,而是切换执行到下一个可执行任务,因此可以实现并发执行。
三.多进程和异步io结合。
经过尝试多线程中没有找到使用异步io的方式,因为eventloop是从当前上下文中提供的,也就是主线程。于是换了个思路,使用多进程,让每个进程的‘主线程’通过异步io来实现并发,经多次尝试后这种方案是可行的。
因为同时运行的进程数量上限是受cpu内核数量的上限影响的,一般的建议是不超过内核数量,但是在一些场景的限制下为了提高效率,单纯受限的多进程不能满足要求的时候,不妨将多进程的‘主进程’结合并发来提高效率。
由于没有实际测试性能,不能断言究竟哪种方式效率更高,毕竟也与任务内容有关。这只是一种思路,留待日后验证。
代码如下:
import asyncio import aiohttp from concurrent.futures import ProcessPoolExecutor, as_completed ,ThreadPoolExecutor import time async def post_http(): # 示例 url = '' data = '' async with aiohttp.ClientSession() as session: async with session.post(url=url, data=data, headers={}, timeout=60) as resp: r_json = await resp.json() return r_json async def t_handler(data, t_flag, p_flag, semaphore): async with semaphore: for d in data: print(f'pid:{p_flag} tid:{t_flag} data:{d}') await asyncio.sleep(1) # 处理费时的io操作,比如httprequest return def p_handler(datas, p_flag): # 线程并发数需要有限制 linux打开文件最大默认为1024 win为509 待确认 ts = time.time() num = 10 # 最大并发数 count = len(datas) block = int(count / num) + 1 tar_datas = [datas[i * block: (i + 1) * block if (i + 1) * block < count else count] for i in range(num)] semaphore = asyncio.Semaphore(num) tasks = [t_handler(d, i, p_flag, semaphore) for i, d in enumerate(tar_datas)] loop = asyncio.get_event_loop() # 基于当前线程 ,故在多线程中无法使用 只能在多进程中使用 loop.run_until_complete(asyncio.wait(tasks)) loop.close() return f' 33[0;32mprocess {p_flag} :cost {time.time() - ts} 33[0m' if __name__ == '__main__': ts = time.time() datas = [i for i in range(1000)] datas = [datas[i * 100:(i + 1) * 100] for i in range(10)] # 每个进程要处理的数据 # 启动异步io 主线程调用 event_loop 在当前线程下启动异步io 实现并发 # res = p_handler(datas,1) # print(res) p_num = 10 block_len = 100 datas = [datas[i * 100:(i + 1) * 100] for i in range(p_num)] # 每个进程要处理的数据 # ProcessPoolExecutor 可能与运行环境有关 官方的 with as 会主动释放线程 导致主线程退出时找不到进程池内进程已经被释放 导致Error in atexit._run_exitfuncs异常 executor = ProcessPoolExecutor(p_num) futures = [executor.submit(p_handler, d, p_flag) for p_flag, d in enumerate(datas)] for f in as_completed(futures): if f.done(): res = f.result() print(res) print(f'Exit!! cost:{time.time() - ts}')