Python模块atexit的应用。
Python中atexit
模块可以注册一些列函数,让Python解析器在恰当的方式退出前调用这些函数。这个功能很实用,例如我们实现“文件锁”功能,通过在文件系统中创建一个文件来表示进程正在运行且确保进行的唯一性。当进程退出时,删除该文件,以告知其他需要创建进行的服务。
另外还可以用于处理异常、日志、销毁进行运行过程的中间文件、临时文件、数据库连接等。
从简单的示例开始 atexit的注册函数的签名如下:
1 atexit.register(func, *args, **kwargs)
使用atexit.register
注册一个进程退出时的回调。
1 2 3 4 5 6 7 8 9 10 import atexitdef all_done : print ("process exit after execute atexit funcs" ) def main (): print ("run main function" ) if __name__ == '__main__' : atexit.register(all_done)
在命令行下运行:
输出:
1 2 3 $python atexit_1.pyrun main function process exit after execute atexit funcs
可以看到在main
函数运行完退出前解析器调用了通过atexit.register
处注册的函数。
接下来一个稍微复杂的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import atexitdef atexit_func_1 (): print ("func_1" ) def atexit_func_2 (): print ("func_2" ) def atexit_func_3 (): print ("func_3" ) def atexit_unregister (): print ("func_unregister" ) def main (): print ("main function" ) if __name__ == '__main__' : atexit.register(atexit_func_1) atexit.register(atexit_func_2) atexit.register(atexit_func_3) atexit.register(atexit_unregister) print ("unregister func:" , atexit_unregister.__name__) atexit.unregister(atexit_unregister) main()
该段代码注册了多个函数,然后取消注册func_unregister
函数,使其从回调栈中删除。
程序执行和输出:
1 2 3 4 5 6 $ python3 atexit_2.py unregister func: atexit_unregister main function func_3 func_2 func_1
通过输出发现,被注册的回调函数的执行次序是注册顺序的反序。如果在程序退出时要执行类事务的操作,需要注意这一点。同一个函数注册多次也会服从这个规律。
不调用atexit注册的函数 很多情况下确保atexit注册的函数被调用很重要,因此我们需要搞明白哪些情况下被注册的函数不会被调用。
它可以为函数func出入参数。不过下面的例子出于简洁性,并不会构造给指定函数出参数的例子。当然,也可以通过偏函数指定参数。
在程序退出时执行atexit
模块注册的函数时有条件的。如果满足如下条件中的任意一条,则atexit
模块不会执行回调函数。
程序调用os._exit()
退出
程序被一个信号终止
检测到解析器的一个致命错误
接下来我们一一举例子。
程序调用os._exit()
退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import atexitimport osdef all_done (): print ("this func should not be called" ) def main (): print ("main func, I call os._exit(1) here" ) os._exit(1 ) if __name__ == '__main__' : print ("register all_done func" ) atexit.register(all_done) main()
main函数中调用了os._exit(1)
阻止了解析器正常退出。
运行和输出:
1 2 3 $python3 atexit_os._exit.pyregister all_done func main func, I call os._exit(1) here
如果要确保程序终止也能调用atexit函数,可以使用sys.exit
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import atexitimport sysdef all_done (): print ("this func should not be called" ) def main (): print ("main func, I call os._exit(1) here" ) sys.exit(1 ) if __name__ == '__main__' : print ("register all_done func" ) atexit.register(all_done) main()
程序被一个信号终止 程序在运行过程中可以通过信号来终止程序,典型的操作是使用kill
命令杀掉程序。接下来这个例子是:父进程创建子进程,然后子进程暂停,等待接收父进程的信号。
我们的子进程代码(atexit_signal.py)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import atexitimport timeimport sysdef all_done (): print ("all done func but not call" ) def main (): print ("run main func" ) sys.stdout.flush() time.sleep(5 ) if __name__ == '__main__' : atexit.register(all_done) main()
父进程代码(atexit_signal_parent.py)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import osimport subprocessimport timeimport signalfunc main(): child = subprocess.Popen('python atexit_signal.py' ) print ("parent pausing before sending signal..." ) time.sleep(2 ) print ("signaling to child" ) os.kill(child.pid, signal.SIGTERM) if __name__ == '__main__' : main()
运行和输出如下:
1 2 3 4 $python3 atexit_signal_parent.pyparent pausing before sending signal... signaling to child run main func
因此,子进程用于接收到父进程的信号而退出并没有调用注册的all_done
函数。
检测到解析器的一个致命错误 致命的错误有很多情况。这些错误都导致解析器无法按照正常的执行流程退出,比如执行到操作系统调用的代码段,程序却没有拿到缺陷而退出。
atexit处理异常 atexit注册的回调函数中产生的异常会输出到控制台上(如果没有捕捉),最后参数的异常会重新抛出。一个简单的例子。
1 2 3 4 5 6 7 8 9 10 import atexitdef quit_exception (): print () raise Exception("atexit with exception" ) if __name__ == '__main__' : atexit.register(quit_exception) atexit.register(quit_exception) print ("start..." )
运行和输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $python3 atexit_traceback.pystart... Error in atexit._run_exitfuncs: Traceback (most recent call last): File "atexit_traceback.py" , line 5, in quit_exception raise Exception("atexit with exception" ) Exception: atexit with exception Error in atexit._run_exitfuncs: Traceback (most recent call last): File "atexit_traceback.py" , line 5, in quit_exception raise Exception("atexit with exception" ) Exception: atexit with exception
类比思考之Go语言 在Go语言中,实现Python的atexit模块功能,有defer
功能。在Go语言中称为延时调用。但和Python不同的是,defer
机制是在声明函数调用的函数执行完毕时调用。
即使函数执行出错了,延时调用也会执行。在Go编程开发中,它能保证资源回收操作被执行。由于延时操作并不是一个简单的CALL调用,会有一定的性能损耗,因此,如果代码对性能要求高,资源回收敏感,通过直接方法执行回收操作更好。
当然强大的Python就怎么能在这一点上认输呢?Python自带的sched
模块可以实现定时事件调度器,一定程度上通过delay来弥补defer
的不足。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" ) func defer1 () { fmt.Println("defer func 1" ) } func defer2 () { fmt.Println("defer func 2" ) } func main () { defer defer1() defer defer2() }
运行和输出:
1 2 3 $go run defer_demo.godefer func 2 defer func 1
Go的defer特性也有一个小问题,看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package main func defer1() { defer defer2() } func defer2() { defer defer1() } func main() { defer defer1() }
如果在实际编程中没有注意这个细节:两个函数互相通过defer
机制注册的对方。程序在时就会出现栈溢出。
1 2 3 4 5 6 7 8 9 10 11 12 $ go run defer_wrap_defer.go runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow runtime stack: runtime.throw(0x46c030, 0xe) ~/go1.9/src/runtime/panic.go:605 +0x9c . . . ...additional frames elided... exit status 2
和Python一样,被defer注册的函数是逆序调用的。Go语言的defer
机制的比Python的atexit
好的地方是其粒度更小:前者注册的函数精确到注册所在函数结束时调用;后者只能在程序退出时调用。
一个应用例子 接下来以一个在实际开发中使用的例子来体验atexit的应用。
Linux下编程常常需要程序以守护进程的方式启动,根据UNIX环境编程知识可知通常的做法(代码层面)就是fork以及会话、权限、I/O设置等。写多了感到挺无聊的,于是采用一个类的方式把这个过程封装一次,程序代码只要重写run方法即可,就像多线程编程中继承Thread类重写run方法。大致流程如下:
fork 退出父进程 (在此前可以处理好缓冲区数据以免带到子进程中)
更改子进程目录、修改文件掩码、更新会话ID设置为首领
fork 退出父进程
I/O重定向设置
管理当前进程的pid(确保其唯一性)
登记进程终止时的处理函数(通常是删除pid文件、释放可能引起安全问题的资源)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import osimport atexitimport signalimport sysclass Daemon : def __init__ (self, *args, pidfile='./center.pid' , stdin='/dev/null' , stdout='/dev/null' , stderr='/dev/null' , **kwargs ): self.pidfile = pidfile self.stdin = stdin self.stdout = stdout self.stderr = stderr self.args = args self.kwargs = kwargs def set_pidfile (self ): if os.path.exists(self.pidfile): with open (self.pidfile, 'r' ) as fp: pid = fp.read() raise RuntimeError("Process[{}] already running" .format (pid)) pid = os.getpid() with open (self.pid, 'w' ) as fp: print (pid, file=fp) atexit.register(lambda : os.remote(self.pidfile)) def fork (self ): try : if os.fork() > 0 : raise SystemExit(0 ) except OSError as err: raise RuntimeError("fork #1 failed." ) os.chdir("/" ) os.umask() os.setsid() try : if os.fork() > 0 : raise SystemExit(0 ) except Exception as err: raise RuntimeError("fork #2 failed." ) def set_stdio (self ): sys.stdout.flush() sys.stderr.flush() with open (self.stdin, 'rb' , 0 ) as fp: os.dup2(fp.fileno(), sys.stdin.fileno()) with open (self.stdout, 'ab' , 0 ) as fp: os.dup2(fp.fileno(), sys.stdout.fileno()) with open (self.stderr, 'ab' , 0 ) as fp: os.dup2(fp.fileno(), sys.stderr.fileno()) def _sigterm_handler (self, signo, frame ): raise SystemExit(0 ) def set_signal (self ): signal.signal(signal.SIGTERM, self._sigterm_handler) def start (self ): self.set_pidfile() self.fork() self.set_stdio() self.set_signal() self.run() def run (self ): pass
该类的I/O重定向默认写到黑洞
上。atexit函数会在程序退出时删掉daemon
进程的pid
文件。确保统一操作系统下只有一个这样的daemon
进程。
这一部分可参看旧文Linux下Python创建守护进程通用类 。
转载请包括本文地址:https://allenwind.github.io/blog/5417 更多文章请参考:https://allenwind.github.io/blog/archives/