localtime接口与localtime_r接口

一、背景

在用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;
}

运行结果如下:

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

相关推荐
鹏大师运维2 小时前
为什么信创电脑装软件总提示“软件包架构不匹配”?
linux·运维·架构·国产化·麒麟·deb·统信uos
007张三丰2 小时前
软件测试专栏(11/20):测试框架开发:pytest深度解析与插件体系
运维·服务器·自动化测试·pytest·测试框架
weixin_604236674 小时前
华三 路由器 极简核心配置
运维·服务器·网络·h3c·h3c路由器
鹤落晴春4 小时前
【Linux复习】管理SELinux安全性
linux·运维·服务器
yz_aiks4 小时前
Linux Jar包配置Systemd自启动实战:从排查到配置全流程
linux·python·jar·自启动·systemd
AI智图坊4 小时前
多件装组合SKU图的批量生产效率分析:从PS手工到AI自动化的工作流改造
大数据·运维·人工智能·gpt·ai作画·自动化·aigc
bjzhang756 小时前
CentOS下安装MySQL详解
linux·mysql·centos
Jason_chen7 小时前
Linux 6.2 音频机制深度解析:AI驱动的低延迟音频与零信任音频安全架构
linux
下午写HelloWorld7 小时前
Linux系统及Ubuntu常用指令
linux·ubuntu·操作系统
lizhihai_998 小时前
股市学习心得-AI 产业链核心标的梳理清单
大数据·服务器·人工智能·科技·学习