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

运行结果如下:

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

相关推荐
HalvmånEver2 小时前
Linux:简介(进程间通信一)
linux·运维·服务器
汽车通信软件大头兵2 小时前
汽车MCU 信息安全--数字证书
服务器·https·ssl
以为不会掉头发的詹同学2 小时前
【TCP通讯加密】TLS/SSL 证书生成、自签名证书、请求 CA 签发证书以及使用 Python TCP 服务器与客户端进行加密通讯
服务器·python·tcp/ip·ssl
阿沁QWQ2 小时前
windows连接服务器免密
运维·服务器
代码游侠2 小时前
学习笔记——数据封包拆包与协议
linux·运维·开发语言·网络·笔记·学习
开开心心_Every2 小时前
定时管理进程:防止沉迷电脑的软件推荐
xml·java·运维·服务器·网络·数据库·excel
云霄IT2 小时前
ssh使用代理连接服务器:基本用法使用ncat
运维·服务器·ssh
FIT2CLOUD飞致云2 小时前
支持IP证书签发、数据库TCP代理,1Panel v2.0.16版本正式发布
linux·运维·服务器·开源·1panel·ip证书
Q741_1472 小时前
Linux UDP 服务端 实战思路 C++ 套接字 源码包含客户端与服务端 游戏服务端开发基础
linux·服务器·c++·游戏·udp