在日志分析脚本中,利用多进程并行处理同一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))
修改后的代码在该实验场景下可以正确的完成任务。
另外,多进程/多线程如果要输出的话,就免不了要对输出进行额外处理,不然输出可能会乱。
这里提供一个思路,就不铺开了:
将执行过程中的输出统一存储到一个对象中,最后通过加锁来实现输出时的串行化
关于这个主题,总感觉我以上探究出来的方法可能不是最优的方法,小伙伴们如果有任何想法欢迎来讨论。

762

被折叠的 条评论
为什么被折叠?



