mktime和localtime_r能在多线程环境下使用么?

本文指出localtime和mktime是标准c函数,用于时间分量和秒数转换。glibc文档称localtime_r和mktime在多线程环境安全,但实际因涉及全局变量,二者线程不安全。分析了代码实现问题,提出自己实现或用pthread加锁两种解决方案,但各有弊端。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

localtime和mktime是用来在时间分量和时间秒数之间进行转换的标准c函数。

在glibc的文档描述中,localtime的实现是使用了一个内部静态缓存来保存结果,所以这是一个不可用于多线程环境的api。glibc提供了一个线程安全版本localtime_r。mktime不存在这个问题。

所以,按照glibc的文档,在多线程环境下可以安全的使用localtime_r和mktime,实际情况并非如此。

mktime和localtime_r在实现上都考虑了时区的转换,而时区的计算要使用全局变量tzname/timezone/daylight。这本质上就是线程不安全的。

参考glibc-2.3.2的源代码(下面的源代码位置都是相对于源码根目录的)

--------- time/localtime.c 和 time/tzset.c

localtime_r中调用了tzset_internal来设置时区,入口参数为always=0,所以理论上只要第一次初次化过了,就不需初始化了。参考下面的代码。
但是由于引入了静态变量is_initialized,在多线程环境下,这种实现代码是有问题的。无法保证并发执行环境下的正确性。


---- (time/tzset.c) -------

/* Interpret the TZ envariable.  */
static void
internal_function
tzset_internal (always)
     int always;
{
  static int is_initialized;
  register const char *tz;
  register size_t l;
  char *tzbuf;
  unsigned short int hh, mm, ss;
  unsigned short int whichrule;

  if (is_initialized && !always)
    return;
  is_initialized = 1;
...........
}

但是mktime不是这样的,

---- (time/mktime.c) -------

/* Convert *TP to a time_t value.  */
time_t
mktime (tp)
     struct tm *tp;
{
#ifdef _LIBC
  /* POSIX.1 8.1.1 requires that whenever mktime() is called, the
     time zone names contained in the external variable `tzname' shall
     be set as if the tzset() function had been called.  */
  __tzset ();
#endif

  return __mktime_internal (tp, my_mktime_localtime_r, &localtime_offset);
}

由于_LIBC被定义,所以tzset将每次都被调用,而tzset的代码是这样的

---- (time/tzset.c) -------

void
__tzset (void)
{
  __libc_lock_lock (tzset_lock);

  tzset_internal (1);

  if (!__use_tzfile)
    {
      /* Set `tzname'.  */
      __tzname[0] = (char *) tz_rules[0].name;
      __tzname[1] = (char *) tz_rules[1].name;
    }

  __libc_lock_unlock (tzset_lock);
}

tzset_internal将每次都被调用,时区信息将每次都被重写。

需要说明的是,前面的宏__libc_lock_lock在sysdeps/generic/bits/libc-lock.h中定义为:
#define __libc_lock_lock(NAME)
是个空操作,所以它不能起到同步线程的作用。

所以,可以看到,glibc的上述代码实现中,有两个问题:
1、tzset_internal 中使用的static变量is_initialized
(这个我们可以通过在程序中定义一个无用的全局变量,在线程开始工作前,它的初始化中调用一次mktime来克服)
2、mktime每次都要重写全局变量tzname/timezone/daylight
(这个问题,基本上,就没办法解决了)

所以mktime和localtime_r不适合于多线程应用。


解决方案有二:
1、自己实现mktime和localtime_r,但是这样时区的计算是麻烦的,当然也可以不使用时区信息,或者使用固定时区,比如北京时区,这样就简单多了。
2、用pthread的mutex来给mktime和localtime_r加锁,但是这样要使用pthread库,移植性不够好。

<think>好的,用户现在想要在Ubuntu下用C++编写一个使用POSIX定时器在每天下午6点异步触发输出当前时间的代码。首先,我需要确认POSIX定时器的相关函数结构,比如timer_create、timer_settime等。用户可能对POSIX定时器不太熟悉,所以需要详细解释步骤。 接下来,用户提到每天18点触发,这意味着需要计算当前时间到下一个18点的时间间隔。可能需要使用gettimeofday或clock_gettime来获取当前时间,然后计算差值。这里需要注意时区的处理,因为用户可能希望根据系统时区来确定18点。 然后,异步触发函数需要用到信号处理,比如使用SIGALRM信号。但POSIX定时器通常使用sigevent结构来指定信号回调,这里可能需要设置sigev_notify为SIGEV_THREAD,这样在定时器触发时,会创建一个新线程执行回调函数。不过,使用线程安全的问题需要考虑,比如cout是否需要加锁。 另外,用户可能希望定时器只触发一次,还是每天重复。这里需要设置定时器的间隔,比如将it_interval设置为24小时,这样定时器会自动重新加载。需要确认struct itimerspec的配置是否正确,特别是tv_sectv_nsec的设置。 代码方面,需要包含必要的头文件,比如<ctime>、<csignal>、<iostream>、<chrono>等。在回调函数中,获取当前时间并输出。要注意在多线程环境下,使用std::cout可能需要同步,但如果是单线程或者SIGEV_THREAD可能不会有问题,但最好加上互斥锁确保安全。 测试部分,可能需要让程序保持运行,比如使用无限循环或等待信号。用户可能需要知道如何编译代码,比如使用g++并链接实时库-lrt。 潜在问题:时区处理是否正确,定时器是否精确,以及是否考虑夏令时等因素。此外,如果系统在18点时处于休眠状态,定时器可能不会触发,需要用户注意。 最后,需要提供一个完整的示例代码,并解释每一步的作用,确保用户能够理解调整参数。可能还需要提醒用户权限问题,比如某些定时器需要root权限,但POSIX定时器一般不需要。</think>以下是在 Ubuntu 下使用 C++ POSIX 定时器实现每天 **18:00 异步触发输出时间**的详细代码及解析: --- ### **核心思路** 1. **POSIX 定时器**:使用 `timer_create` + `timer_settime` 创建定时器。 2. **时间计算**:计算当前时间到当天 18:00 的剩余时间。 3. **信号回调**:通过 `SIGEV_THREAD` 实现异步触发。 --- ### **完整代码** ```cpp #include <iostream> #include <ctime> #include <csignal> #include <cstring> #include <chrono> #include <thread> #include <stdexcept> // 定时器回调函数 void timer_handler(union sigval sv) { // 获取当前时间并输出 auto now = std::chrono::system_clock::now(); std::time_t now_time = std::chrono::system_clock::to_time_t(now); std::cout << "当前时间: " << std::ctime(&now_time); } // 计算到下一个 18:00 的时间间隔(秒) time_t get_seconds_to_18() { auto now = std::chrono::system_clock::now(); std::time_t now_time = std::chrono::system_clock::to_time_t(now); std::tm* local_tm = std::localtime(&now_time); // 如果当前时间已经超过 18:00,则计算明天的 18:00 if (local_tm->tm_hour >= 18) { local_tm->tm_mday += 1; } // 设置目标时间为当天的 18:00:00 local_tm->tm_hour = 18; local_tm->tm_min = 0; local_tm->tm_sec = 0; std::time_t target_time = std::mktime(local_tm); return target_time - now_time; } int main() { // 创建定时器 timer_t timerid; struct sigevent sev; struct itimerspec its; // 设置定时器属性 memset(&sev, 0, sizeof(sev)); sev.sigev_notify = SIGEV_THREAD; // 异步线程回调 sev.sigev_notify_function = timer_handler; // 回调函数 sev.sigev_notify_attributes = nullptr; if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) { throw std::runtime_error("Failed to create timer"); } // 初始化定时器触发时间 time_t initial_sec = get_seconds_to_18(); its.it_value.tv_sec = initial_sec; // 首次触发时间 its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 24 * 3600; // 每天重复一次(24小时) its.it_interval.tv_nsec = 0; // 启动定时器 if (timer_settime(timerid, 0, &its, nullptr) == -1) { throw std::runtime_error("Failed to start timer"); } // 保持主线程运行(否则程序会退出) while (true) { std::this_thread::sleep_for(std::chrono::hours(24)); } return 0; } ``` --- ### **关键步骤解释** 1. **定时器初始化**: - `timer_create` 创建一个基于 `CLOCK_REALTIME`(系统实时时间)的定时器。 - `SIGEV_THREAD` 表示定时器触发时创建一个新线程执行回调函数。 2. **时间计算**: - `get_seconds_to_18()` 计算当前时间到当天 18:00 的剩余秒数。 - 如果当前时间已过 18:00,则计算到次日 18:00 的时间。 3. **定时器配置**: - `its.it_value` 设置首次触发时间(距离 18:00 的秒数)。 - `its.it_interval` 设置间隔时间为 24 小时,实现每天触发。 4. **保持程序运行**: - 主线程通过 `while (true)` 循环保持运行,防止程序退出。 --- ### **编译与运行** ```bash # 使用 g++ 编译(需链接实时库 -lrt) g++ timer_example.cpp -o timer_example -lrt -pthread # 运行程序 ./timer_example ``` --- ### **注意事项** 1. **时区问题**: - 代码使用 `std::localtime` 获取本地时间,确保系统时区设置正确。 2. **线程安全**: - `std::cout` 是线程安全的,但在高并发场景中仍需谨慎。 3. **精准性**: - 定时器精度依赖系统时钟,可能受系统负载影响。 4. **退出控制**: - 可通过信号(如 `SIGINT`)捕获实现优雅退出。 --- ### **扩展改进** - **错误处理**:增加 `try-catch` 捕获异常。 - **日志记录**:将输出替换为日志文件写入。 - **动态时间调整**:支持运行时修改触发时间。 如果需要进一步优化或有其他需求,请随时告知!
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值