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打印时间代码片段如下,
实际上两处陷阱导致的上述现象:
- 【原因1】:使用的localtime()函数是非线程安全的,channel_mutex只是为trace部分加了锁,不能保证与其它线程的localtime()调用互斥。
- 【原因2】:Linux中localtime()、gmtime()和ctime()三个函数都是非线程安全的,而且三个函数内部共用了同一个静态区作为返回值(见以下函数说明)。
所以,localtime()、gmtime()和ctime()三个函数不能多线程并行调用,否则会相互干扰。
而gbase中的确还有其它线程在使用gmtime,例如以下代码片段(不仅限这一处),
验证实验
先将本地时区设置为+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),可以观察到相同现象,下图所示,
解决方法
将非线程安全的函数替换成线程安全的版本,例如,localtimer代替localtime,gmtimer代替gmtime、ctime_r代替ctime。
【注】:这三个线程安全函数内部有锁。
Tip
- 多线程并行调用localtime为什么不能复现问题?咱们代码中,转换的time都是当前时间,不同线程取到的当前时间非常接近,难以观察到时间转换错误,实际上转换结果错误。
- 多线程并行调用localtime和gmtime为什么可以复现问题?因为localtime转换使用本地时区,而gmtime转换使用UTC时区,当本地时区与UTC时区不一样时,才会观察到错误(相差小时级)。
- trace打印时间的代码源自IB原型,IB错了?应该不是,因为IB以单线程计算为主,且已经保证了localtime此类非线程安全函数不会并行调用。另外,早先Linux以多进程开发为主,起源较早的应用软件代码并不太关注多线程问题。
问题延伸
gbase的原型IB,使用了不仅限以上所述的非线程安全函数,随着更多代码被多线程化,以及第三方库的引入,其中的隐患也在增加,原本不是问题的小地方也会成为问题。后续,我们将调用此类问题。
打赏作者以资鼓励: