python 多进程和异步io的有机结合 Error in atexit._run_exitfuncs

  众所周知,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'\033[0;32mprocess {p_flag} :cost {time.time() - ts}\033[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}')

  

posted @ 2020-12-03 15:18  单亚林  阅读(2303)  评论(0编辑  收藏  举报