一、背景
在用C接口获取本地时间时,一般是先获取墙上时间,比如通过clock_gettime(CLOCK_REALTIME, ...);这样获取到timespec结构的时间,然后再通过本地时间的转换接口,拿到本地时间。
localtime接口常用于做这样的转换,这个接口在使用上比较便捷,即直接返回了获取到的本地时间的struct tm的指针,这样你可以相对方便地直接使用数据。
但是,需要特别注意的是,这个接口并不是线程安全的,如果一个进程内的两个线程同时调用这个接口来获取时间,那么就可能会出现获取到的时间数据出现不一致性。
我们在第二章里使用一个例子来复现这样的现象,在第三章里进行原理解释。
二、案例
2.1 案例源码
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <unistd.h>
struct tm* gettime_unsafe(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return localtime(&ts.tv_sec);
}
volatile bool _thread1_continue_run = false;
volatile bool _thread2_should_run = false;
void gettime_thread1(void)
{
struct tm *tm_info;
int min, sec;
tm_info = gettime_unsafe();
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
printf("thread1: min=%d, sec=%d\n", min, sec);
// wait until second is 57 or 58
while (1)
{
tm_info = gettime_unsafe();
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
if (sec == 57 || sec == 58) {
break;
}
sleep(1);
}
tm_info = gettime_unsafe();
sec = tm_info->tm_sec;
// after get struct tm pointer, notify thread2 to run localtime at the same time
_thread2_should_run = true;
while (!_thread1_continue_run);
min = tm_info->tm_min;
printf("thread1: min=%d, sec=%d\n", min, sec);
}
void gettime_thread2(void)
{
struct tm *tm_info;
int min, sec;
while (!_thread2_should_run);
while (1)
{
tm_info = gettime_unsafe();
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
if (sec == 0 || sec == 1) {
printf("thread2: min=%d, sec=%d\n", min, sec);
_thread1_continue_run = true;
break;
}
sleep(1);
}
}
int main()
{
std::thread thread1(gettime_thread1);
std::thread thread2(gettime_thread2);
thread1.join();
thread2.join();
return 0;
}
2.2 案例现象
如下图:

可以从上图里的第一个红色框打印出来的分钟和秒数,打印出来的是12分57秒,明显超过了当前的date时间。可见多线程下同时使用localtime接口进行本地时间的转换会导致时间获取获取的数值的不一致,即获取了前一个时刻的秒,但是获取了后一个时刻的分,所谓数据一致性出错。
三、原理解释及例子修改
3.1 相关原理
这是因为glibc在实现localtime接口时,返回出来的指针其实是一个进程内部是同一个地址,导致进程内多个线程同时改写这个指针指向的值时出现数据不一致情况。
glibc的localtime的实现,通过elf文件如下图:

可以看到是localtime其实直接就是调用的__localtime64接口:
看一下__localtime64接口的实现:

上图里可以看到__localtime64其实就是调用了__tz_convert接口,要注意,传入给__tz_convert接口的第三个参数&_tmbuf其实是一个全局变量:

所以,每次localtime的调用,都传递了相同的地址作为第三个参数给到__tz_convert接口,而__tz_convert接口其实就是把这个传入的第三个参数,作为返回值返回出去的,如下图:

所以就会有localtime的线程安全的问题。
而使用localtime_r就没有这个问题,我们看一下localtime_r的实现,从elf里可以看到:

localtime_r是调用的__localtime64_r接口,而__localtime64_r接口的实现如下:

它是把传入给__localtime64_r的第二个参数作为参数透传给__tz_convert函数的,所以,只要localtime_r用的是局部变量来作为第二个参数传入,就不会有线程安全的问题。
3.2 改造后的线程安全的版本的例子及运行结果
修改后的例子的源码:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <unistd.h>
void gettime_safe(struct tm *tm)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
localtime_r(&ts.tv_sec, tm);
}
volatile bool _thread1_continue_run = false;
volatile bool _thread2_should_run = false;
void gettime_thread1(void)
{
struct tm tm;
struct tm *tm_info = &tm;
int min, sec;
gettime_safe(tm_info);
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
printf("thread1: min=%d, sec=%d\n", min, sec);
// wait until second is 57 or 58
while (1)
{
gettime_safe(tm_info);
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
if (sec == 57 || sec == 58) {
break;
}
sleep(1);
}
gettime_safe(tm_info);
sec = tm_info->tm_sec;
// after get struct tm pointer, notify thread2 to run localtime at the same time
_thread2_should_run = true;
while (!_thread1_continue_run);
min = tm_info->tm_min;
printf("thread1: min=%d, sec=%d\n", min, sec);
}
void gettime_thread2(void)
{
struct tm tm;
struct tm *tm_info = &tm;
int min, sec;
while (!_thread2_should_run);
while (1)
{
gettime_safe(tm_info);
sec = (int)tm_info->tm_sec;
min = (int)tm_info->tm_min;
if (sec == 0 || sec == 1) {
printf("thread2: min=%d, sec=%d\n", min, sec);
_thread1_continue_run = true;
break;
}
sleep(1);
}
}
int main()
{
std::thread thread1(gettime_thread1);
std::thread thread2(gettime_thread2);
thread1.join();
thread2.join();
return 0;
}
运行结果如下:

可以看到,修改后就不在存在线程安全的问题了。