之前为了诊断系统的检索性能中的问题,分阶段地对程序进行了计时,诊断出的结果是:记录越多,那么这三个阶段所消耗的时间就越长。而反复review三个阶段的主要工作代码,却发现这些代码根本不可能这么耗时。慢慢地,顺藤摸瓜地查出时间消耗都发生在获取时间值的方法——mktime()上。于是大家一致怀疑是mktime()方法以及相关的localtime_r()方法都不是线程安全的。可其他项目中也用这些方法,为什么他们就没有发现这样的问题呢?
在glibc的文档描述中,glibc提供了一个线程安全的方法localtime_r来代替localtime。mktime不存在线程不安全的问题。所以,按照glibc的文档,在多线程环境下可以安全的使用localtime_r和mktime,实际情况并非如此。找来mktime()和localtime_r()的源代码,终于发现了奥妙所在。
mktime和localtime_r在实现上都考虑了时区的转换,而时区的计算要使用全局变量tzname/timezone/daylight。这本质上就是线程不安全的。
通过对glibc中这两个系统函数源代码的分析可以知道,它们实现中有两个问题:
1、tzset_internal 中使用的static变量is_initialized
2、mktime每次都要重写全局变量tzname/timezone/daylight
所以mktime和localtime_r不适合于多线程应用。
解决方案有二:
1、自己实现mktime和localtime_r,但是这样时区的计算是麻烦的,当然也可以不使用时区信息,或者使用固定时区,比如北京时区,这样就简单多了。由于公司的服务器基本不会有时区问题,因此我们也自己实现了这两个方法。
2、用pthread的mutex来给mktime和localtime_r加锁,但是这样要使用pthread库,移植性不够好。gettimeofday()这个syscall用来供用户获取timeval格式的当前时间信息(精确度为微秒级),以及系统的当前时区信息(timezone)。结构类型 timeval的指针参数tv指向接受时间信息的用户空间缓冲区,参数tz是一个timezone结构类型的指针,指向接收时区信息的用户空间缓冲区。这 两个参数均为输出参数,返回值0表示成功,返回负值表示出错。
首先来看一下spin_lock机制。spin_lock机制和semaphore机制解决的都是两个进程的互斥问题,都是让一个进程退出临界区后另一个进程才进入的方法,不过sempahore机制实行的是让进程 暂时让出cpu,进入等待队列等待的策略,而spin_lock实行的却是却进程在原地空转,等着另一个进程结束的策略。
gettimeofday()代码中的read_lock_irqsave(lock, flags)以及read_lock_irq(lock), read_lock_bh(lock) 和 write_lock_irqsave(lock, flags) , write_lock_irq(lock), write_lock_bh(lock)都是spin_lock的一个小小的变型,而Spin_lock采用的方式是让一个进程运行,另外的进程忙等待,由于在只有一个cpu的机器(UP)上微观上只有一个进程在运行。因此gettimeofday()也不是线程安全的系统调用。