当前位置:C++技术网 > 资讯 > 日志技术的问题总结和完整改进方案(单线程、多线程、子线程、定时器、队列的运用)

日志技术的问题总结和完整改进方案(单线程、多线程、子线程、定时器、队列的运用)

更新时间:2019-04-08 14:38:29浏览次数:1+次

    在讨论主线程、多(子)线程、定时器、队列的运用之前,我们先来看使用的一个场景,即:

        在程序的运行过程中,我们需要知道程序运行的各个状态,想知道是否按照预期在运行了,我们需要一种方法得知。如果是控制台程序,我们打印到屏幕即可。如果是后台程序,没有办法打印到屏幕,那么我们一般就需要打日志了。打日志也就是将一些信息写入到日志文件里,并没有什么复杂的。一些框架可能会提供一些日志工具,就不用我们自己去写打印日志的代码了。但是如果自己写框架呢?或者框架并没有提供呢?那么我们需要自己写打日志的代码,也就是包装一个写文件的函数而已。

    所以我们的应用场景之一就是打日志,我们就这个场景来说一下主线程、多(子)线程、定时器、队列的运用。

    我们的程序需要不间断的运行,我们还需要不间断的打日志记录运行的状态,方便随时查看或者崩溃后查看分析。问题来了,我们的程序可能有bug导致崩溃,那么日志就是为了记录这种问题,好让我们分析程序崩溃的原因的。所以打日志是不能影响到程序正常运行的,更不能因为打日志出现bug导致程序崩溃了。现实的情况是,我们打日志因为一些地方没有处理好,经常出现崩溃,进而把程序牵连进来了。去掉日志也不好,没有日志记录,程序一旦崩溃,我们没有任何日志可查。当然其他手段不再这里讨论。比如Linux下可以有coredump文件,可以在C++技术网的精品专题的“coredump”专题了解相关的操作。

    那么我们要做的就是把打日志的功能做好做稳。下面看看两种通常使用打日志的具体场景。

1.单(主)线程打日志

    使用主线程的单线程打日志,意味着你的程序仅在主线程打日志,要么是单线程进程,要么是多线程进程但仅仅是主线程打了日志。通常情况下,新手程序员一般就是这种方式。

    写好一个打日志的函数,然后就到处在用了。

    表面看上去并没有什么问题。仔细想想,针对前面的场景,这种方式是存在极大的隐患的。我们的程序就一个线程或者主线程里直接打日志。打日志是一个辅助的操作,千万不能影响主程序的运行的。但是我们程序就一个主线程,而打日志只在主线程里进行,如果是线程自身崩溃,那么我们的日志刚好起到可以分析的作用,而如果因为日志崩溃进而导致主程序崩溃,那就得不偿失了。

    也就是说,直接在主线程里进行日志操作,是非常危险的操作。在写demo程序的时候是没有问题的,如果是真正运行的程序,是万万不能的。这个不是夸夸其谈。我之前开发的服务器程序,开始就是这样做的。中间因为几次打日志而崩溃,导致整个程序崩溃,让线上的服务瘫痪,所以只能重新启动程序来恢复了。我们不是说服务程序就一定不会挂,但是不要因为辅助的操作引发崩溃,否则辅助的意义何在呢?这个问题需要引起重视,这是宝贵的经验,一般人不会告诉你的。这是看似普通的经验,却可以帮助到你极大提高程序的稳定性。打日志只是其中一个例子,凡是辅助的操作一定不要对主程序产生干扰,更不能引发灾难。否则不如不要。


2.多线程打日志

    对于我们使用了多线程的场景,我们更是要注意了。比如一个主线程是为了做一个登记工作,具体的工作将会使用子线程来处理,以免主线程在处理事情导致整个程序卡住假死了。所以多线程的作用不言而喻。对于多线程程序,我们也需要跟踪各个环节的运行情况,也是需要打日志的。我们打日志还是用封装好的打日志的函数。也就是说,多个线程,都会用这个打日志的函数来打日志。

    多线程有多线程的问题,对于多个线程可能同时执行了打印日志的操作,需要进行文件的互斥操作。如果没有互斥,就可能带来文件操作的冲突。比如第一个线程没有以独占形式打开文件,第二个线程也打开了文件,那么两个线程都会对文件进行操作,结果文件的内容最后是混乱的。甚至说会因此导致主进程崩溃。如果加了互斥锁,也可能产生死锁而卡死程序。

    所以多线程的程序,对程序员要求变高了,需要处理好互斥操作临界资源的问题,还要防止死锁。多线程程序可能有更多的bug,并不适合新手。单线程相比之下简单的多,但是存在单线程因打日志而崩溃的问题。多线程如果没有互斥访问和死锁的问题,尽管子线程打日志挂了,也不影响主线程继续运行。但是如主线程参与了打日志,那么其实多线程程序加重了风险,比单线程程序更容易崩溃。

    单线程和多线程打日志是两种常见的应用场景。

    现在就是改善打日志的方案上场的时候了。
1.子线程打日志

    这里的子线程打日志,和前面说的多线程的打日志是不一样的。我们这里改进的办法就是,用专用的一个线程来打日志,其他的线程都不打日志。凡是需要打日志的时候,都启动一个新线程,然后用这个线程来打日志。因为我们前面包装过一个打日志的函数,所以我们也就只需要在这个函数里来创建线程就可以了。其他地方和改进之后的用法一模一样的。

    使用子线程来打日志的好处在于,就算子线程崩溃了,也不影响主线程的运行。打日志本来就是辅助操作,少一次并不太影响分析,所以,即使打日志的代码有点缺陷,也不用太担心。

    这种方法也有自身的不足。每一次打日志,都会创建一个线程,线程执行完后就退出了。反复的打日志也就要反复的创建和销毁线程,必然会降低一些性能,不过对于程序稳定而言,这点损失还是值得的。如果想改进,也是可以的。那就用上线程池。不过这个难度自然是比较高的。不管是用现成的线程池模块还是自己实现,都不简单,自己实现更是实力的挑战。新手就不要想了。

    既然这种方案有点难,那么可以换其他的方案。

2.定时器打日志

    定时器是系统或框架给我们提供的一种定时触发的事件回调机制。对于我们来讲,我们就不需要再创建线程了。首先从技术实现上,已经变得简单了。定时器是系统或框架维持的一个东西,对于我们程序来说,不需要承担线程的创建销毁带来的性能损失,还是不错的。我们可以将定时器简单理解为系统的一个线程(不一定对,可以类似来看),这个线程会循环往复的去走时,然后在时间到了之后执行我们的定时器函数。对于定时器的可靠性,我们是信得过的,至少比我们自己写的线程函数要健全的多了。而这种底层的功能,自然性能和稳定性都是非常高的。我们用好了,性价比很高。

    所以,我们可以得到一个信息,那就是,无论如何,在一个间隔之后,系统总会触发一个定时器事件,然后来执行我们的定时器回调函数。这种回调是可靠的稳定的。如果我们的定时器回调函数崩溃了,下次还是会照常来的。所以这样也不会影响主程序的运行,对我们的程序的稳定性是一个很大的提升。这一点比子线程打日志好很多了, 在稳定性、性能上都有提升。所以这种方案自然比子线程打日志好很多。

3.使用队列

    那么优化好打日志的方式之后,我们最终采用定时器来打日志。但是有一个问题,我们的日志是调函数来打印的。这个怎么和定时器关联在一起呢?我们虽然确定了定时器方案,但是具体实现还没有完全说清楚。打印日志的函数是随时被调用的,而打日志是在定时器里定时执行的。这样也就产生了冲突。一个是随时被调用执行,一个是定时执行,在时间上就有了空缺。

    那么我们就需要一个缓冲了,让缓冲来弥补这个空缺。当缓冲了一定的数据之后,就将这个实时执行的操作和定时执行的操作给连接起来了。那么实时执行的只是一个打日志的准备,真正执行的是定时打日志的这个操作。因为定时可以将间隔设置的很短,所以看上去还是比较实时的。

    所以,我们在打日志的函数里,不再是创建子线程了,而是将要输出的数据投递到一个消息队列里,然后定时器定时去取消息,然后打印日志。这样的改动,我们只是将打日志的代码移到了定时器回调函数里,然后在打日志的代码里换成投递消息的代码。这样就实现了这个功能。

    使用定时器和队列,那么定时器打日志的方案就完成了。
    
4.单线程+队列+死循环+创建子线程

    鉴于前面的分析,如果你不喜欢定时器,或者某种条件下无法使用定时器,不必灰心。我们依然有办法哈。我们先创建一个写日志的子线程,然后线程函数里死循环读取队列里的数据。读取也可以采用定时方式,只不过是以线程休眠的方式。如果队列里没有数据,则睡眠一会,有数据就一直读取数据,然后写日志。为了防止写日志的线程崩溃,导致后续没法写日志了,我们可以在投递数据到队列的时候进行检测,一旦写日志的线程挂掉了,就新建一个线程来写日志。这样可以持续不断的运转下去,和定时器的方案就差不多了,只是实现起来需要点时间哈。

    不过对于一般情况,我们尽量选择定时器(含队列)方案,是最省事省力的。而队列只是一个自定义的存储数据的东西,不一定是系统的消息队列哈。