最近需要做定时任务,同事推荐了Python的一个库叫做schedule,用它来做定时还是比较简单的。
比如,像这样傻瓜式的:
schedule.every().day.at("10:36").do(daily_job_A)
简单运用在我的代码,但是前两天运行中发现一个现象:如果定义了两个启动时间相差1min的task且前1个task不能在1min内执行完毕,那么后一个将一直被阻塞直到前一个执行完,也就是说定时到了也不能启动。
由于我的task是爬虫程序会运行很久,这样一个接一个当然不行。
不过为了验证,先写个demo复现一下上述现象是否确确实实存在:
import schedule
import time
def daily_job_A():
print("A启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(300)
print("A完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def weekly_job_A():
print("A启动2" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(300)
print("A2完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def daily_job_B():
print("B启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(300)
print("B完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def weekly_job_B():
print("B启动2" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(300)
print("B2完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
if __name__ == "__main__":
schedule.every().day.at("10:36").do(daily_job_A)
schedule.every().sunday.at("10:37").do(weekly_job_A)
schedule.every().day.at("11:38").do(daily_job_B)
schedule.every().sunday.at("11:39").do(weekly_job_B)
while True:
schedule.run_pending()
time.sleep(1)运行效果……A2本来该在10:37启动,但实际上被阻塞到A完毕
好了,现象确实存在。
那怎么解决呢?
一开始是准备从schedule的层面解决,因为按照这样的调用方式schedule显然没有对每个任务去开新的进程,所以这些任务们只好排队,等着这一个进程有空;但转念一想,开新进程这事也可以在自己的应用层面解决——把定时任务的任务编程新开子进程。
(一般定时任务是在操作系统层面做到的吧,这里用schecule其实就是假设操作系统层面做不到的话,在python层面去做。也是借鉴这个思路,schedule的API这个层面解决不了,再往上层去做;TCP协议有一些问题,于是在应用层做一些事情……)
试一试,修改如下:
(本来准备用subprocess.call结果发现是命令行形式的启动,于是用了multiprocessing.Process)
import schedule
import time
from multiprocessing import Process
def daily_job_A():
print("A启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
Process = time.sleep(120)
print("A完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def weekly_job_A():
print("A2启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(120)
print("A2完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def daily_job_B():
print("B启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(120)
print("B完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def weekly_job_B():
print("B2启动" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
time.sleep(120)
print("B2完毕" + time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
def daily_job():
p1 = Process(target=daily_job_A)
p2 = Process(target=daily_job_B)
p1.start()
print("p1.start")
p2.start()
print("p2.start")
def weekly_job():
p3 = Process(target=weekly_job_A)
p4 = Process(target=weekly_job_B)
p3.start()
print("p3.start")
p4.start()
print("p4.start")
if __name__ == "__main__":
schedule.every().day.at("11:9").do(daily_job)
schedule.every().sunday.at("11:10").do(weekly_job)
while True:
schedule.run_pending()
time.sleep(1)
这下成功啦,效果如下:A和B是同时启动,A2和B2也是同时启动,而且在A和B没有结束的时候A2和B2也启动了
【实现要点】
schedule对各个定时任务是按阻塞式执行,我没有改变这一点,但是改变了定时任务,让定时任务从需要执行很久变成很快执行完毕——具体来说,这里的A和B是我们想要定时的任务,但是任务执行会花去2min,如果直接把A和B当做定时任务第二个就会被阻塞到2min以后才开始,所以类似于偷换了一个概念——把“启动A”和“启动B”做成了定时任务。
当定时任务的内容只是p1.start(),也就是【启动】一个子进程,这是瞬间可以做完的事情。至于子进程p1自己要执行多久都没有关系,因为这里没有p1.join(),主进程就不会等子进程结束,自己继续往下执行,去启动p2, 然后返回,因为对主进程来说它的所有步骤在这个时候都执行完毕了。
【另外注意到 主进程结束 并没有影响子进程】
这很好。因为以前写例子都会在p.start()之后跟上p.join(),意思是等子进程结束以后主进程再往下走。而这里希望主进程不要等待,所以没有设p.join(),开始会有担心子进程会随父进程而消失而消失,毕竟是派生出来的,结果证明不会。
应用到自己的代码,改进!!!
刚才是windows环境,保险起见,还是去ubuntu上试一下,以防有坑:
没问题的(*^▽^*)
最后附一个可以打印进程PID的例子备查。虽然和要解决的问题没有太大关系:
from multiprocessing import Process
import os
def info(title):
print(title)
print('module name:', __name__)
if hasattr(os, 'getppid'): # only available on Unix
print('parent process:', os.getppid())
print('process id:', os.getpid())
def f(name):
info('function f')
print('hello', name)
if __name__ == '__main__':
info('main line')
p = Process(target=f, args=('bob',))
p.start()
p.join()