Python 进程池中嵌套使用线程池

日志分析脚本中,利用多进程并行处理同一server上的多个日志文件

但是一个子进程处理一份日志文件过程中,亦有大量循环处理(串行)的情况,所以考虑若在该子进程中将一些任务改为多线程,应该可以获得较明显的提升。

就下来使进程池中嵌套线程池的一些研究实验

来看一个示例脚本

from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Process,Pool
import time

max_workers = 10
# tp=ThreadPoolExecutor(max_workers=max_workers)

def sleep_(j, k):
    print(__name__)
    time.sleep(j)
    print('{}: sleep  '.format(k))

def main(n):
    # 注意tp的三个位置,看结果放到哪里都一样
    # 但是放到这里,两个子进程调用的tp对象是不同的
    tp = ThreadPoolExecutor(max_workers=10)
    for i in range(10):
        res = tp.submit(sleep_,3,i)    #非阻塞

if __name__ == '__main__':
    # tp = ThreadPoolExecutor(max_workers=10)
    p = Pool(processes=2)
    p.map(main,(1,2))    #阻塞,要等待所有子进程都执行完毕
    # time.sleep(5)    #这个时间比3大就行

然而,上述代码并没有像我们期望的那样等待3s,然后打印输出,而是“瞬间”就执行完毕,sleep_中的print语句并未执行
注意:
最后一行这个sleep如果不存在,则上述代码sleep_函数中sleep之后的代码不会执行。
表象原因:ThreadPoolExecutor.submit()方法不阻塞,所以Pool.map()方法能够不用等待sleep_函数中的sleep,而很快的执行完(虽然Pool.map本身是等待所有函数执行完),进而脚本就执行完了。脚本都执行完了,所有的对象/资源都就被销毁了,自然执行不到sleep_中sleep之后语句。


经试验,单独的多线程脚本不会出现这种问题

from concurrent.futures import ThreadPoolExecutor
import time

tp=ThreadPoolExecutor(max_workers=10)

def sleep_(j, k):
    time.sleep(j)
    print('{}: sleep  '.format(k))

for i in range(10):
    a=tp.submit(sleep_,3,i)

执行该脚本,可以清楚的看到
1. 并不是tp.submit()之后就相当于完成了,主进程并没有在for循环执行完就退出,而是等待sleep
2. 线程池大小(10)的作用,当循环30次时则几乎分成3批执行,每次“同时”执行10个
说明上面对于“表象”的解释不完全正确


再来看通过sleep来阻塞主进程会发生什么

from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Process,Pool
import time

tp=ThreadPoolExecutor(max_workers=10)

def sleep_(j, k):
    print(__name__)
    time.sleep(j)
    print('{}: sleep  '.format(k))

def main(n):
    for i in range(30):
        res=tp.submit(sleep_,3,i)

if __name__ == '__main__':
    p=Pool(processes=2)
    p.map(main,(1,2))
    time.sleep(4)

针对以上代码,注意最后一行sleep:
1. 如果该sleep时间大于完成所有循环所需的总时间(以及循环次数和线程池大小可计算)例如9s,则可以得到期望的结果
2. 如果该sleep时间小于完成所有循环所需的总时间,例如4s(两个子进程中线程池分别为10,循环tp.submit30次只有前10次能完整执行)
print('{}: sleep '.format(k))则在两个子进程中只能执行10+10=20次(这个好解释)
print(__name__)在两个子进程中会执行共(10+10)+(10+10)=40次(前3s解释同上,剩下的1s只够两个子进程各自执行10次sleep_中sleep之前的代码)

又说明上文对于“表象”的解释,也不完全错。就差在了“子进程”上


想到一种方法可以避开此坑,看修改过后的代码

from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Pool
import time
import os

max_workers = 10
tp=ThreadPoolExecutor(max_workers=max_workers)

def sleep_(j, k):
    """线程池调用的函数"""
    time.sleep(j)
    print('{}: sleep  '.format(k))


def main(n):
    """进程池调用的函数"""
    res_list=[]

    for i in range(19):
        res=tp.submit(sleep_,3,i)
        res_list.append(res)

    #以下循环是multiprocessing.Pool情况下再嵌套concurrent.futures.ThreadPoolExecutor时必须的,
    #否则子进程不会等待多线程执行完
    for j in res_list:
        '''在子进程调用main执行完毕之前
        通过调用future.result()循环阻塞每个子线程。
        只是这里第一次阻塞的时间里,其他线程基本上也就完成了(相同的)任务,
        所以这里可以认为只有一次sleep_函数的执行时间'''
        j.result()

    #子进程最后会执行该输出
    print('{}------'.format(os.getpid()))

if __name__ == '__main__':
    p=Pool(processes=2)
    p.map(main,(1,2))

修改后的代码在该实验场景下可以正确的完成任务。


另外,多进程/多线程如果要输出的话,就免不了要对输出进行额外处理,不然输出可能会乱。
这里提供一个思路,就不铺开了:

将执行过程中的输出统一存储到一个对象中,最后通过加锁来实现输出时的串行化

关于这个主题,总感觉我以上探究出来的方法可能不是最优的方法,小伙伴们如果有任何想法欢迎来讨论。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值