python 模块 多线程 单例_python Logger模块单例模式

本文介绍了Python内置的Logger模块在多线程环境中的线程安全特性,以及Logger的单例模式。官方推荐通过`logging.getLogger(__name__)`获取Logger实例,确保在进程中始终返回同一个对象。此外,文章还展示了如何手动实现Logger的单例模式,并分析了在实现过程中需要注意的初始化问题,以及在多线程环境下日志持久化可能遇到的数据丢失问题及其解决方案。
摘要由CSDN通过智能技术生成

前言

提前祝大家过个好年

最近忙于项目,今天抽出点时间写写Blog谈谈昨天遇到的问题

项目最近要收尾了,想把Logger规整一下,因为很多地方都有用到

Python的Logger模块是Python自带的模块,可方便快捷的进行日志的记录

python doc

正文

线程安全

该模块本身就是线程安全的,下面的注释摘抄至 doc

The logging module is intended to be thread-safe without any special work needing to be done by its clients. It achieves this though using threading locks; there is one lock to serialize access to the module’s shared data, and each handler also creates a lock to serialize access to its underlying I/O.

If you are implementing asynchronous signal handlers using the signal module, you may not be able to use logging from within such handlers. This is because lock implementations in the threading module are not always re-entrant, and so cannot be invoked from such signal handlers.

也就是说你不需要关注多线程的问题,只要 getLogger() 时指定当前空间即可

Loggers have the following attributes and methods. Note that Loggers should NEVER be instantiated directly, but always through the module-level function logging.getLogger(name). Multiple calls to getLogger() with the same name will always return a reference to the same Logger object.

The name is potentially a period-separated hierarchical value, like foo.bar.baz (though it could also be just plain foo, for example). Loggers that are further down in the hierarchical list are children of loggers higher up in the list. For example, given a logger with a name of foo, loggers with names of foo.bar, foo.bar.baz, and foo.bam are all descendants of foo. The logger name hierarchy is analogous to the Python package hierarchy, and identical to it if you organise your loggers on a per-module basis using the recommended construction logging.getLogger(name). That’s because in a module, name is the module’s name in the Python package namespace.

意思是 logger.getLogger() 时传入相同的变量,会永远返回同一个对象,比如我在当前进程内的任何地方, 使用 log = logger.getLogger("work") 生成的log对象一直是同一个对象,这就是单例模式,官方推荐传入 __name__ 因为他是Python包命名空间中模块的名称。

如果你是纠结 Logger 的单例怎么解决,你可以关闭网页了,因为他本身是单例的

手动写一个单例

手动写一个单例完全是为了记忆单例模式的使用,只是以 logger 模块举例

原始代码

不考虑 logger 的自带单例情况下的原始代码

bfefcb6e19afb6ba4d6c9445810ced41.png

代码精简过,大致意思不变

测试代码

测试是否可以使用的代码

10fe2b3feb67add3ca1c1af4382c422b.png

利用__new__实现单例

我们知道,python实例化时其实是先走 __new__ 再走 __init__

我们可以重写 __new__ 方法,如果发现已生成对象直接返回该对象

同时为了防止多线程的资源竞争,我们使用线程锁来保证同一时间只有一个线程能访问 __new__

50a0a22c3c3cafe3e27457ad727b80f8.png

但是测试代码跑过之后发现每次会输出接近100条日志,这是为什么呢?

原来,每次请求实例化时,如有对象则直接返回之前生成的对象(MyLogger._instance),但是因为 Python3 默认继承新式类,

即 Object ,每次请求时返回了 object.__new__ 然后会再执行一遍 MyLogger 的 __init__ 方法,而我们在 __init__ 中添加了两个 Handler ,

而上文提到, logger.getLogger 传入同一个参数则 logger 为一个, 导致每次请求时都会添加两个 Handler 到同一个 logger ,这样导致 logger 的 Handler 越来越多,重复写入了,解决这个问题需要防止重复走 __init__

其实正确的写法应该是类的 __init__ 只负责接收参数,像这种 add Handler 的功能放到自写方法中,这样 __init__ 不会有任何 add 操作即可

利用元类继承实现单例

该方法利用元类 Type的 __call__ 实例化的对象调用不会走 __init__ 的特性来规避问题

0ae9d7abdb8b4250a155466621fa5fba.png

如果你觉得本方法需要覆盖父类不太好,那么还有第三种方法

自写初始化方法

方法1中,每次都会走 __init__,而我们在 __init__ 中又进行了 add Handler 等操作,那么我们将所有初始化及 add 操作放在自写方法中即可

f89c65d49e2b2e6f01cfc58403d3625b.png

如上图所示,这对请求实例化的用户是无感知的,它只需要和之前一样调,但其实内部在实例化时调用了 start, 同时重复实例化时走 __init__ 没有任何代码逻辑(走的Object)

小彩蛋

我们在实际测试过程中发现,在配置了log持久化存储搭配多线程使用的时候,写入log文件的日志会丢失数据,测试发现应该是实例化后立刻写入会出现一些延迟,再加上测试代码

写入一条后立刻结束,导致的丢失问题,当然,在实际使用中,一般是初始化时实例化,也不可能在log后直接停止

但是问题还是要解决,实例化我们在每次请求实例化时等待一下即可 time.sleep(1) ,等待时间与机器性能有关,好的机器不会出现问题,

1s是保险的

然后最终代码为

ba4e2bf45ff390870c7dc4c94e83f598.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值