ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++获取系统时间全指南:从time()到chrono的线程安全与精度实践

C++获取系统时间全指南:从time()到chrono的线程安全与精度实践 1. 从一次日志时间戳错乱说起前阵子帮一个做量化回测的朋友排查问题他写了个C程序记录每笔模拟交易的成交时刻跑完之后发现日志里的时间戳居然出现了“时间倒流”——后一条记录的时间比前一条还早。他第一反应是系统时钟被同步服务校正了查了半天才发现问题出在他用了std::time(nullptr)配合std::localtime而localtime返回的是一个指向静态缓冲区的指针在多线程环境下被反复覆盖导致写日志时拿到的结构体内容已经变了。这个坑其实很典型。C里获取当前系统时间看起来是个再基础不过的需求但真要把这件事做对、做稳、做准里面涉及的标准库设施、平台差异、精度选择、线程安全、时区处理足够写一篇长文。我这些年写过日志系统、性能计时器、定时任务调度、跨平台数据采集程序几乎每个项目都要跟时间打交道踩过的坑五花八门。这篇文章就把C获取系统时间的几条主流路径彻底捋一遍从最传统的C风格接口到C11之后的chrono体系再到C20的日历扩展配上实测代码和选型建议争取让你看完之后不再为“取个时间”纠结。不管你是刚学C不久、还在用time()加ctime的新手还是已经用惯了chrono但对其底层机制一知半解的进阶开发者下面这些内容应该都能帮你把这块知识补完整。我会尽量把每个接口的适用场景、精度边界、线程安全性、跨平台表现讲清楚并且给出可以直接复制运行的代码片段。2. C风格时间接口老而弥坚但边界要摸清2.1 time()与tm结构体最小可用的时间获取方案最原始的获取当前时间的方式就是调用std::time。它返回一个std::time_t类型的值在绝大多数平台上这个类型就是自Unix纪元1970-01-01 00:00:00 UTC以来的秒数。代码简单到不能再简单#include ctime #include iostream int main() { std::time_t now std::time(nullptr); std::cout 秒级时间戳: now std::endl; return 0; }这个秒数本身是UTC时间跟时区无关适合做时间差计算、排序、存储。但人眼没法直接读所以通常要转成可读格式。这就轮到std::localtime和std::gmtime出场了。前者转成本地时区后者转成UTC返回的都是std::tm*指针。std::tm结构体里包含年从1900起算、月0到11、日、时、分、秒等字段。注意两个反直觉的点年份要加1900才是真实年份月份要加1才是人类习惯的月份。我见过不少新手在这里写错导致日志里出现“2024年0月”这种诡异输出。std::time_t now std::time(nullptr); std::tm* local std::localtime(now); std::cout (local-tm_year 1900) - (local-tm_mon 1) - local-tm_mday local-tm_hour : local-tm_min : local-tm_sec std::endl;2.2 localtime的线程安全陷阱与替代方案前面提到的那个“时间倒流”问题根源就在std::localtime。C标准库里的localtime、gmtime、asctime、ctime这几个函数内部都使用了一个静态分配的结构体来存放结果。也就是说你调用一次拿到指针再调用一次前一次的指针指向的内容就被覆盖了。单线程顺序执行时没问题但一旦多线程并发调用或者你在两次调用之间穿插了别的调用数据就可能错乱。解决方式有几种。第一种是用平台提供的线程安全版本POSIX系统上有localtime_r和gmtime_rWindows上有localtime_s和gmtime_s。注意这两个平台的函数签名不一样POSIX版本是localtime_r(const time_t*, struct tm*)Windows版本是localtime_s(struct tm*, const time_t*)参数顺序相反跨平台代码里需要条件编译。std::time_t now std::time(nullptr); std::tm tm_buf{}; #ifdef _WIN32 localtime_s(tm_buf, now); #else localtime_r(now, tm_buf); #endif第二种方式是干脆绕开这些C接口直接用C的chrono库配合C20的时区功能或者手动做时区偏移计算。后面会详细讲。注意如果你的程序只在单线程里顺序调用时间转换函数并且拿到结果后立刻使用、不跨调用保存指针那么用localtime也不是不行。但只要涉及多线程或者结果需要保存就必须换成线程安全版本。2.3 strftime格式化灵活但容易踩格式串的坑拿到tm结构体之后格式化输出通常用std::strftime。它支持一大堆格式说明符比如%Y四位年、%m两位月、%d两位日、%H二十四小时制、%M分、%S秒、%z时区偏移等。用起来很灵活char buf[64]; std::strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, tm_buf); std::cout buf std::endl;但这里有几个坑。第一缓冲区大小要留够格式串越长、输出越复杂需要的空间越大给太小会截断甚至返回0表示失败。第二%z和%Z的输出在不同平台上格式不完全一致做跨平台日志解析时要注意。第三strftime本身是线程安全的只要传入的tm是你自己的缓冲区但它依赖的locale设置可能受全局状态影响如果程序里改过locale输出格式可能变化。实测下来如果只是做固定格式的日志时间戳我一般会手动用snprintf拼接避免strftime的locale依赖和格式串解析开销。比如char buf[32]; std::snprintf(buf, sizeof(buf), %04d-%02d-%02d %02d:%02d:%02d, tm_buf.tm_year 1900, tm_buf.tm_mon 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec);这样输出格式完全可控性能也更好。缺点是时区信息需要自己处理不过对于本地时间日志来说通常够用。3. chrono体系精度、类型安全与跨平台一致性3.1 system_clock与steady_clock的本质区别C11引入的chrono库是获取时间的现代方式。里面最常用的两个时钟是std::chrono::system_clock和std::chrono::steady_clock。很多人分不清该用哪个这里把它们的本质区别讲透。system_clock表示的是“墙上时钟”也就是跟现实世界日期时间对应的时钟。它可以被系统管理员手动调整也可能被网络时间同步服务校正。所以用它来测量时间间隔是不靠谱的——两次调用之间如果系统时间被往前调了算出来的间隔可能是负数。steady_clock表示的是“单调时钟”它保证永远只往前走不受系统时间调整影响。它的起点是某个未指定的时刻通常是系统启动时所以不能用来获取现实日期但非常适合做性能计时、超时判断、心跳间隔测量。// 获取现实时间用于日志时间戳 auto now std::chrono::system_clock::now(); // 测量代码耗时用于性能分析 auto start std::chrono::steady_clock::now(); // ... 执行某些操作 ... auto end std::chrono::steady_clock::now(); auto elapsed std::chrono::duration_caststd::chrono::milliseconds(end - start); std::cout 耗时: elapsed.count() ms std::endl;还有一个high_resolution_clock它在大多数实现上就是system_clock或steady_clock的别名标准并没有规定它必须是单调的所以我不建议在需要明确语义的场合使用它。选型原则很简单要现实时间用system_clock要计时用steady_clock。3.2 从time_point到可读字符串的完整转换链路system_clock::now()返回的是一个time_point要转成人类可读的字符串需要经过几步转换。在C20之前标准库没有提供直接的时区转换和格式化工具通常的做法是先转成time_t再走C风格的localtime和strftimeauto now std::chrono::system_clock::now(); std::time_t t std::chrono::system_clock::to_time_t(now); std::tm tm_buf{}; #ifdef _WIN32 localtime_s(tm_buf, t); #else localtime_r(t, tm_buf); #endif char buf[64]; std::strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, tm_buf);这条链路能用但有个明显缺陷to_time_t会丢失亚秒精度。如果你需要毫秒或微秒级的时间戳就得单独把亚秒部分取出来拼接auto now std::chrono::system_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds( now.time_since_epoch()) % 1000; std::time_t t std::chrono::system_clock::to_time_t(now); std::tm tm_buf{}; localtime_r(t, tm_buf); char buf[80]; std::snprintf(buf, sizeof(buf), %04d-%02d-%02d %02d:%02d:%02d.%03d, tm_buf.tm_year 1900, tm_buf.tm_mon 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec, static_castint(ms.count()));这段代码在Linux和macOS上跑没问题Windows上把localtime_r换成localtime_s即可。实测精度取决于系统时钟粒度Linux上通常能到微秒级Windows上默认时钟中断粒度大约是15.6毫秒需要更高精度的话得用QueryPerformanceCounter或者调整系统计时器分辨率。3.3 duration_cast的截断行为与精度选择duration_cast做的是截断转换不是四舍五入。比如你把毫秒转成秒1500毫秒会变成1秒而不是2秒。这个行为在计算时间差时通常没问题但如果你需要更精确的舍入就得自己处理余数。auto ms std::chrono::milliseconds(1500); auto sec std::chrono::duration_caststd::chrono::seconds(ms); // sec.count() 1不是2选择精度时要考虑实际需求和系统能力。做日志时间戳毫秒级通常够用做高频交易或性能剖析可能需要微秒甚至纳秒级。但要注意system_clock的实际分辨率受操作系统限制你请求纳秒精度拿到的值可能仍然是微秒粒度的重复值。可以用下面这段代码测一下你所在平台的实际分辨率auto t1 std::chrono::system_clock::now(); auto t2 std::chrono::system_clock::now(); while (t2 t1) { t2 std::chrono::system_clock::now(); } auto diff std::chrono::duration_caststd::chrono::nanoseconds(t2 - t1); std::cout 时钟分辨率约: diff.count() ns std::endl;我在几台机器上测过Linux服务器上通常在几十到几百纳秒普通Windows台式机上可能到十几毫秒。这个数据对你选择时间戳精度很有参考价值。4. C20日历与时区终于不用手动算偏移了4.1 year_month_day与hh_mm_ss的配合使用C20在chrono里加入了一套日历类型包括year_month_day、hh_mm_ss、weekday等让日期时间的拆解和格式化变得直观很多。比如要获取当前日期的年月日#include chrono #include iostream int main() { auto now std::chrono::system_clock::now(); auto today std::chrono::floorstd::chrono::days(now); std::chrono::year_month_day ymd{today}; std::cout static_castint(ymd.year()) - static_castunsigned(ymd.month()) - static_castunsigned(ymd.day()) std::endl; return 0; }floordays把时间点截断到当天零点然后构造year_month_day。注意year、month、day这些类型不能直接当整数输出需要显式转换。这套接口的好处是类型安全编译器能帮你挡住不少逻辑错误比如把月份当成日期用。要获取时分秒可以用hh_mm_ssauto now std::chrono::system_clock::now(); auto today std::chrono::floorstd::chrono::days(now); std::chrono::hh_mm_ss hms{now - today}; std::cout hms.hours().count() : hms.minutes().count() : hms.seconds().count() std::endl;不过要注意hh_mm_ss默认输出的是UTC时间要转本地时区还得配合zoned_time。4.2 zoned_time处理本地时区的正确姿势C20的std::chrono::zoned_time是处理时区的利器。它把时间点和时区信息绑在一起可以自动处理夏令时切换等复杂情况#include chrono #include iostream int main() { auto now std::chrono::system_clock::now(); std::chrono::zoned_time zt{std::chrono::current_zone(), now}; std::cout zt std::endl; return 0; }current_zone()返回系统当前时区输出格式类似2024-06-15 14:30:25.123456789 CST。如果你知道目标时区也可以直接指定比如std::chrono::locate_zone(Asia/Shanghai)。但这里有个现实问题截至我写这篇文章时GCC对C20时区支持还不完整需要链接-ltz或者使用较新版本Clang配合libc的支持也参差不齐MSVC相对好一些。所以如果你的项目需要跨平台且不能保证编译器版本C20时区功能可能还不能放心用。我的建议是新项目可以尝试但要做好降级方案老项目继续用C风格接口加手动时区处理稳当。4.3 格式化输出std::format与chrono的联动C20的std::format可以直接格式化chrono类型语法比strftime更直观auto now std::chrono::system_clock::now(); std::string s std::format({:%Y-%m-%d %H:%M:%S}, now);但同样受编译器支持限制。GCC 13以上、MSVC 19.29以上才比较完整。如果不能用std::format可以退回strftime或者手动拼接。我在实际项目里通常会封装一个format_time函数内部根据编译器和平台选择可用方案对外提供统一接口。这样上层代码不用关心底层用的是哪套API。5. 多线程环境下的时间获取实战5.1 每线程独立缓冲区避免数据竞争多线程程序里获取时间最核心的原则是不要共享任何可变的时间转换缓冲区。每个线程用自己的tm结构体和字符缓冲区互不干扰。下面是一个线程安全的日志时间戳函数std::string get_current_timestamp() { auto now std::chrono::system_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds( now.time_since_epoch()) % 1000; std::time_t t std::chrono::system_clock::to_time_t(now); std::tm tm_buf{}; #ifdef _WIN32 localtime_s(tm_buf, t); #else localtime_r(t, tm_buf); #endif char buf[80]; std::snprintf(buf, sizeof(buf), %04d-%02d-%02d %02d:%02d:%02d.%03d, tm_buf.tm_year 1900, tm_buf.tm_mon 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec, static_castint(ms.count())); return std::string(buf); }这个函数每次调用都在栈上分配tm_buf和buf不涉及任何静态或全局状态天然线程安全。返回std::string而不是char*也避免了悬空指针问题。实测在8线程并发压测下每秒调用百万次没有出现任何时间错乱。5.2 高频调用场景下的性能优化如果你的程序需要每秒获取几十万次时间戳比如高频日志或性能采样上面那个函数可能成为瓶颈。主要开销在localtime_r和snprintf上。优化思路有几个第一如果只需要时间戳数值而不需要可读字符串直接用system_clock::now().time_since_epoch()省掉所有转换。第二如果日志格式固定可以缓存时区偏移。localtime_r每次都要查时区数据库开销不小。可以在程序启动时算一次本地时区偏移之后直接用UTC时间加偏移量。但要注意夏令时切换的日子需要重新计算。第三用无锁队列把时间戳生成和日志写入解耦让专门的时间线程批量生成时间戳其他线程直接取用。这个方案复杂一些适合对性能有极致要求的场景。我做过一个测试在Linux上直接调system_clock::now()大约20纳秒加上localtime_r和snprintf完整格式化大约200到300纳秒。如果你的程序每秒调用十万次那就是20到30毫秒的额外开销不算大但也不能忽视。百万次的话就值得优化了。5.3 时间回拨的检测与应对即使你用system_clock获取现实时间也要意识到它可能被系统时间同步服务往回调整。对于日志系统来说时间回拨会导致日志顺序混乱。一个实用的做法是记录上一次的时间戳如果发现当前时间比上一次早就标记异常或者使用单调时钟补偿。class TimeMonitor { std::chrono::system_clock::time_point last_; bool first_ true; public: bool check_and_update() { auto now std::chrono::system_clock::now(); bool ok true; if (!first_ now last_) { ok false; // 检测到时间回拨 } last_ now; first_ false; return ok; } };这个类不是线程安全的多线程下需要加锁或者用原子操作。检测到回拨后怎么处理取决于业务需求可以记录警告、可以用单调时钟继续计时、也可以暂停日志写入等待时间恢复。我在一个数据采集项目里用的是“记录警告但继续写入”的策略因为采集不能停时间戳异常总比丢数据好。6. 跨平台差异与编译器兼容性清单6.1 Windows与POSIX的时间API对照跨平台开发时时间相关的API差异主要集中在几个地方。下面这张表整理了我经常遇到的对照关系功能WindowsPOSIX (Linux/macOS)线程安全localtimelocaltime_s(tm, t)localtime_r(t, tm)线程安全gmtimegmtime_s(tm, t)gmtime_r(t, tm)高精度计时QueryPerformanceCounterclock_gettime(CLOCK_MONOTONIC)系统时间GetSystemTimePreciseAsFileTimeclock_gettime(CLOCK_REALTIME)时区名称_get_tznametzsettzname注意Windows的localtime_s参数顺序跟POSIX的localtime_r相反这是最容易写错的地方。我通常会用宏封装inline void safe_localtime(std::tm* tm_buf, const std::time_t* t) { #ifdef _WIN32 localtime_s(tm_buf, t); #else localtime_r(t, tm_buf); #endif }这样上层代码统一调用safe_localtime不用到处写条件编译。6.2 各编译器对C20 chrono特性的支持现状C20的chrono日历和时区功能虽然好但编译器支持情况参差不齐。根据我的实测MSVC 19.29及以上日历类型基本可用时区功能需要Windows 10 1903以上且需要定义_SILENCE_CXX20_CISO646_REMOVED_WARNING之类的宏来避免警告。GCC 13及以上日历类型可用时区功能需要链接-ltz且部分发行版需要额外安装时区数据包。Clang 16配合libc 16日历类型部分可用时区功能支持不完整。所以如果你的项目需要支持较老的编译器或者嵌入式平台建议还是以C11的chrono加C风格接口为主C20特性作为可选增强。我一般会在CMake里检测编译器版本和特性支持用__cpp_lib_chrono宏来判断#if defined(__cpp_lib_chrono) __cpp_lib_chrono 201907L // 可以使用C20日历和时区功能 #else // 降级到C风格接口 #endif6.3 嵌入式与特殊环境的注意事项在嵌入式环境里时间获取又是另一番景象。很多嵌入式系统没有实时时钟芯片或者系统启动后时间从某个固定值开始。这时候system_clock返回的时间可能毫无意义。你需要先确认硬件是否有RTC以及系统是否在启动时从RTC同步了时间。另外嵌入式工具链的标准库实现可能不完整localtime_r、strftime这些函数可能缺失或者行为不一致。我遇到过一个ARM工具链strftime的%z输出为空字符串导致日志解析脚本出错。后来改成手动计算时区偏移才解决。还有一种情况是实时操作系统任务调度可能影响时间获取的实时性。如果对时间戳精度要求极高可能需要直接读硬件定时器寄存器而不是走标准库。7. 选型决策什么场景该用什么方案7.1 按精度需求选择接口精度需求是选型的第一个维度。下面这张表可以作为快速参考精度需求推荐方案备注秒级std::timelocaltime_r最简单兼容性最好毫秒级system_clock 手动拼接日志时间戳的常见选择微秒级system_clockduration_cast注意平台实际分辨率纳秒级steady_clockduration_cast仅用于计时不用于现实时间超高精度计时平台专用API如QueryPerformanceCounter选精度时不要盲目追求高够用就行。精度越高代码越复杂跨平台问题越多。我见过一个项目为了“未来可能需要”而用了纳秒级时间戳结果在Windows上精度根本达不到反而引入了不必要的复杂度。7.2 按线程模型选择安全策略单线程程序可以放心用localtime但多线程程序必须用线程安全版本。如果你的程序是“一个线程写日志、多个线程产生日志”的模型那么写日志的线程可以独占一个tm缓冲区其他线程把时间戳数值传过来由写线程统一转换。这样既安全又高效。如果每个线程都要独立生成时间戳字符串那就每个线程用自己的栈上缓冲区像前面get_current_timestamp那样。不要用全局或静态缓冲区除非你加了锁但加锁又会成为性能瓶颈。7.3 按可维护性选择抽象层级从可维护性角度我建议在项目里封装一个统一的时间工具模块对外暴露几个简单函数namespace time_util { // 获取当前时间戳毫秒 int64_t now_ms(); // 获取当前时间戳微秒 int64_t now_us(); // 获取可读时间字符串 std::string now_string(); // 格式化指定时间点 std::string format(std::chrono::system_clock::time_point tp); }这样上层业务代码不直接依赖任何具体的时间API将来要换实现或者适配新平台只改这一个模块就行。我在多个项目里都采用了这种模式维护成本明显降低。8. 那些年我踩过的时间坑8.1 时区设置错误导致的日志时间偏差有一次部署新服务日志时间比实际时间差了8小时。查了半天代码没问题最后发现是容器环境的TZ环境变量没设置系统默认用了UTC。localtime_r读的是系统时区设置容器里如果没挂载宿主机的时区文件就会用默认值。解决办法是在Dockerfile里设置ENV TZAsia/Shanghai或者把宿主机的/etc/localtime挂载进去。这个坑的教训是不要假设运行环境的时区跟你开发机一致。日志系统最好同时记录UTC时间戳和本地时间字符串这样即使时区配置错了也能通过UTC时间戳还原真实时间。8.2 夏令时切换当天的重复时间戳夏令时切换那天本地时间会出现重复或跳过。比如某些时区在凌晨2点把时钟拨回1点那么1点到2点之间的时间戳会出现两次。如果你的日志系统只用本地时间字符串就无法区分这两个时间段。解决办法是记录UTC时间戳作为主键本地时间只作为展示用。C20的zoned_time能自动处理夏令时但前提是时区数据库是最新的。有些老系统上的时区数据库可能过时导致切换日期计算错误。如果对夏令时敏感建议定期更新系统的时区数据包。8.3 高并发下时间戳重复的问题在极高频调用下system_clock的粒度可能导致连续多次调用返回相同的时间戳。这在做唯一ID生成或者事件排序时可能造成问题。解决办法是结合单调计数器时间戳相同的时候用递增序号区分。class UniqueTimestamp { std::atomicint64_t last_{0}; std::atomicint seq_{0}; public: int64_t next() { auto now std::chrono::duration_caststd::chrono::milliseconds( std::chrono::system_clock::now().time_since_epoch()).count(); int64_t expected last_.load(); if (now expected) { if (last_.compare_exchange_strong(expected, now)) { seq_ 0; return now; } } return last_.load() * 1000 (seq_); } };这段代码不是完美的但思路可以参考用时间戳做高位用序号做低位保证单调递增。实际项目中我会用更严谨的无锁实现但核心思想是一样的。8.4 性能计时用错时钟的典型案例前面提过system_clock可能被调整但实际项目中还是经常看到有人用它来测耗时。我接手过一个性能分析工具它用system_clock记录函数开始和结束时间结果在系统时间同步的瞬间某些函数的“耗时”变成了负数或者几小时。后来全部改成steady_clock才正常。记住一个简单规则凡是测量“经过了多久”一律用steady_clock凡是获取“现在几点”才用system_clock。这条规则能帮你避开绝大多数时间相关的逻辑错误。9. 封装一个趁手的时间工具类9.1 接口设计与实现要点基于上面的经验我通常会封装一个轻量的时间工具类提供几个最常用的功能。接口设计原则是简单、线程安全、跨平台、无外部依赖。下面是一个简化版的实现骨架class Clock { public: // 当前毫秒时间戳 static int64_t now_ms() { return std::chrono::duration_caststd::chrono::milliseconds( std::chrono::system_clock::now().time_since_epoch()).count(); } // 当前微秒时间戳 static int64_t now_us() { return std::chrono::duration_caststd::chrono::microseconds( std::chrono::system_clock::now().time_since_epoch()).count(); } // 可读时间字符串格式2024-06-15 14:30:25.123 static std::string now_string() { auto now std::chrono::system_clock::now(); auto ms std::chrono::duration_caststd::chrono::milliseconds( now.time_since_epoch()) % 1000; std::time_t t std::chrono::system_clock::to_time_t(now); std::tm tm_buf{}; #ifdef _WIN32 localtime_s(tm_buf, t); #else localtime_r(t, tm_buf); #endif char buf[80]; std::snprintf(buf, sizeof(buf), %04d-%02d-%02d %02d:%02d:%02d.%03d, tm_buf.tm_year 1900, tm_buf.tm_mon 1, tm_buf.tm_mday, tm_buf.tm_hour, tm_buf.tm_min, tm_buf.tm_sec, static_castint(ms.count())); return std::string(buf); } // 单调时钟用于计时 static int64_t steady_ms() { return std::chrono::duration_caststd::chrono::milliseconds( std::chrono::steady_clock::now().time_since_epoch()).count(); } };这个类所有方法都是静态的无状态天然线程安全。now_string每次调用都在栈上分配缓冲区不涉及共享状态。实测在Linux和Windows上都能正常工作精度满足日志需求。9.2 单元测试与边界验证时间相关的代码测试起来有点麻烦因为结果依赖运行时刻。我的做法是把“获取当前时间”和“格式化时间”分开测试。格式化部分传入固定的time_point验证输出字符串是否符合预期。获取部分只验证单调性和合理性比如连续两次调用时间戳不递减、时间戳在合理范围内。void test_format() { // 构造一个已知时间点2024-01-01 00:00:00 UTC std::tm tm_buf{}; tm_buf.tm_year 2024 - 1900; tm_buf.tm_mon 0; tm_buf.tm_mday 1; tm_buf.tm_hour 0; tm_buf.tm_min 0; tm_buf.tm_sec 0; std::time_t t timegm(tm_buf); auto tp std::chrono::system_clock::from_time_t(t); // 验证格式化结果... }注意timegm是GNU扩展Windows上要用_mkgmtime。测试代码里也要做平台适配。9.3 在日志系统中的应用示例最后给一个在日志系统里使用这个工具类的例子。假设我们有一个简单的日志宏#define LOG_INFO(msg) \ std::cout [ Clock::now_string() ] [INFO] msg std::endl // 使用 LOG_INFO(服务启动完成); // 输出[2024-06-15 14:30:25.123] [INFO] 服务启动完成这个宏在多线程下也是安全的因为Clock::now_string()每次返回独立的字符串对象std::cout的并发写入由标准库保证不会交错C11起要求std::cout的并发访问不产生数据竞争但输出内容可能交错如果需要严格有序还是要加锁或者用专门的日志库。如果要记录耗时可以配合steady_msauto start Clock::steady_ms(); // ... 执行操作 ... auto cost Clock::steady_ms() - start; LOG_INFO(操作耗时 cost ms);这套组合我在多个项目里用了好几年稳定可靠没再出现过时间错乱的问题。核心就是两点现实时间用system_clock加线程安全转换计时用steady_clock封装成无状态工具类上层随便调。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进