FAIRYFAR-INTERNAL
 
  FAIRYFAR-INTERNAL  |  SITEMAP  |  ABOUT-ME  |  HOME  
Linux系统localtime()等时间函数使用陷阱

1. Linux中各种时间函数

UTC时间

协调时间时(Coordinated Universal Time),又称国际标准时间、世界统一时间等,英文可缩写为CUT、UTC、TUC等。

GMT时间

即,格林威治时间。同UTC时间。

时间函数

  • time():返回时间戳,以UTC 1970-01-01 00:00:00到当前时间的时间差。
  • gmtime():返回UTC时间。非线程安全。
  • localtime():返回本地时间。非线程安全。
  • ctime():作用等价于asctime(localtime())。非线程安全。
  • asctime():将时间转换成字符串。非线程安全。
  • mktime():本地时间转换成时间戳。
  • strftime():时间按格式转换成字符串,有sprintf()作用。

【注】:localtime、gmtime和ctime返回的是同一个静态对象区,该静态区会被下一次调用覆盖。

2. 时间函数使用陷阱

问题来源

农行作业链,单机trace,偶尔会出现日志时间跳变16个小时,然后又恢复正常现象,可复现。例如,以下两段trace标红部分。

img

img

原因

trace打印时间代码片段如下,

img

实际上两处陷阱导致的上述现象:

  • 【原因1】:使用的localtime()函数是非线程安全的,channel_mutex只是为trace部分加了锁,不能保证与其它线程的localtime()调用互斥。
  • 【原因2】:Linux中localtime()、gmtime()和ctime()三个函数都是非线程安全的,而且三个函数内部共用了同一个静态区作为返回值(见以下函数说明)。

img

所以,localtime()、gmtime()和ctime()三个函数不能多线程并行调用,否则会相互干扰。

而gbase中的确还有其它线程在使用gmtime,例如以下代码片段(不仅限这一处),

img

验证实验

先将本地时区设置为+8区(Asia/Shanghai)。编写一个多线程小程序,代码片段如下,

snippet.cpp
// 线程0执行以下代码:
time_t curtime = time(NULL);
struct tm* cdt = localtime(&curtime);
char strtime[25] = "";
sprintf(strtime, "%02d:%02d:%02d", cdt->tm_hour, cdt->tm_min, cdt->sec);
printf("thread [0] time is %s\n", strtime);
 
// 线程1执行以下代码:
time_t curtime = time(NULL);
struct tm* cdt = gmtime(&curtime);
char strtime[25] = "";
sprintf(strtime, "%02d:%02d:%02d", cdt->tm_hour, cdt->tm_min, cdt->sec);

这里我们只打印线程0输出,线程1起干扰作用。运行此程序(当前系统时间是09:12),可以观察到相同现象,下图所示,

img

解决方法

将非线程安全的函数替换成线程安全的版本,例如,localtimer代替localtime,gmtimer代替gmtime、ctime_r代替ctime。

【注】:这三个线程安全函数内部有锁。

Tip

  1. 多线程并行调用localtime为什么不能复现问题?咱们代码中,转换的time都是当前时间,不同线程取到的当前时间非常接近,难以观察到时间转换错误,实际上转换结果错误。
  2. 多线程并行调用localtime和gmtime为什么可以复现问题?因为localtime转换使用本地时区,而gmtime转换使用UTC时区,当本地时区与UTC时区不一样时,才会观察到错误(相差小时级)。
  3. trace打印时间的代码源自IB原型,IB错了?应该不是,因为IB以单线程计算为主,且已经保证了localtime此类非线程安全函数不会并行调用。另外,早先Linux以多进程开发为主,起源较早的应用软件代码并不太关注多线程问题。

问题延伸

gbase的原型IB,使用了不仅限以上所述的非线程安全函数,随着更多代码被多线程化,以及第三方库的引入,其中的隐患也在增加,原本不是问题的小地方也会成为问题。后续,我们将调用此类问题。



打赏作者以资鼓励: