我可以传递给std::线程::Slear_for()和Slear_Until()的最大值是多少?

2022-05-23 00:00:00 sleep c++ chrono

This question关于永远睡眠的答案提到了这一点:

std::this_thread::sleep_until(
    std::chrono::time_point<std::chrono::system_clock>::max());

和这个:

std::this_thread::sleep_for(
    std::chrono::system_clock::durat??ion::max());

在Visual C++2017 RC上运行此代码实际上根本不会休眠。我尚未签出sleep_until()案例,因此我不确定那里发生了什么。

sleep_for()的情况下,给定的duration似乎通过将其添加到system_clock::now()而转换为绝对时间,然后将其转发给sleep_until()。问题是加法溢出,给出了一个过去的时间。

查看30.3.2中的C++17草案,sleep_until()sleep_for()似乎都没有提到限制。计时规范(30.2.4)中没有相关内容。对于duration::max()duration_values(20.17.4.3)中描述为:"返回的值应大于zero()",这没有任何帮助。

老实说,我很惊讶地看到sleep_for()system_clock::duration::max()中失败了,因为这是一个对我来说非常有意义的构造。

我可以传递给行为定义明确的函数的最大值是多少?


解决方案

它未指定,将溢出

我与VisualC++标准库开发人员之一billy O‘Neal和libc++的主要作者Howard Hinnant进行了讨论。我的结论是,线程库中的_for_until族将以未指定的方式溢出,您不应尝试向它们传递较大的值。我不清楚该标准是否在这个问题上规定得不够具体。

问题

所有计时函数1都接受aduration或atime_point。两者都由其基础类型(表示)和比率(句点)定义。也可以将周期视为单位,如秒或纳秒。

可能发生溢出的主要位置有两个:

  1. 在平台特定的调用之前,和
  2. 转换为平台特定类型期间

调用前

在这种情况下可以避免溢出,就像Howard在回答中提到的那样,但实现者仍在学习如何在不同精度的持续时间之间进行chrono转换而导致的溢出。

例如,Visual C++2017通过将给定的持续时间添加到 system_clock::now()。如果持续时间太长,则会溢出。其他库(如libstdc++)似乎没有此问题。

系统调用

一旦深入,您就必须与您所在的任何平台进行交互才能完成实际工作。这就是事情变得混乱的地方。

例如,在libstdc++上,对sleep_for()的调用以nanosleep()结束,它接受timespec。这是它的简化版本:

auto s = duration_cast<seconds>(time);
auto ns = duration_cast<nanoseconds>(time - s);

timespec ts = { s.count(), ns.count() };
nanosleep(&ts, &ts);

这很容易溢出:您只需传递一个比Llong_Max秒更长的时间:

std::this_thread::sleep_for(hours::max());
这会使duration_cast溢出到seconds,并将ts.tv_sec设置为-3600,这根本不会休眠,因为nanosleep()在负值时失败。对于sleep_until(),它甚至更好,它试图在循环中调用nanosleep(),但它一直失败,因此在等待期间它占用了100%的处理器。

同样的事情也发生在Visual C++2017库中。忽略sleep_for()中的溢出,因为它将持续时间添加到当前时间,因此它最终调用Sleep,该函数接受以毫秒为单位的32位无符号值。

即使它调用像NtWaitForSingleObject()这样更灵活的值(将来可能会这样),它仍然只是一个以100纳秒为增量的有符号64位值,并且仍然可能溢出。

错误和限制

我个人认为<chrono>库中的溢出本身是一个错误,就像Visual C++实现sleep_for()一样。我认为无论您给出什么值,在调用特定于平台的函数之前,都应该在最终转换之前保持不变。

但是,一旦您到达那里,如果平台不支持在您要求的时间内休眠,就没有真正的解决方案。由于<chrono>被禁止抛出异常,所以我接受溢出的可能性。尽管这将成为未定义的行为,但我希望实现能够更小心地处理溢出,例如libstdc++在处理EINVAL和在紧密循环中旋转方面的各种失败。

Visual C++

我引用了我从比利・奥尼尔那里收到的电子邮件中的一些东西,因为它们增加了标准库开发人员的观点:

你是说:

this_thread::sleep_for(system_clock::durat?ion::max());

标准中是否有未定义的行为?

据我所知,是的。这是一种灰色区域--实际上并没有为这些函数指定最大允许范围,但是考虑到它们接受任意time_point/duration的本质,它可能得到一些用户提供的bignum类型的支持,而标准库对此一无所知,因此转换成一些底层的time_point/duration类型基本上是强制的。<chrono>的设计将处理溢出视为非目标(例如,请参阅duration_cast,它直接禁止实现,就好像无穷大和类似情况一样)。

标准[...]没有给我们任何方式来报告这里没有转换--行为从字面上来说是未定义的。我们被明确禁止抛出异常,如果超过LLONG_MAX会发生什么,我们无法进行推理,因此我们唯一可能的回应是,就像无穷大一样,或者直接转到std::terminate(),不通过GO,不收取200美元。

Libstdc++和libc++的目标平台是实际映射到平台理解的东西的平台,其中Unix时间戳是国家法律。我们的目标不是这样的平台,我们有义务在DWORD毫秒和/或FILETIME之间进行映射。

我能想到的唯一合理的用例可能是拥有某种类型的前端值,这意味着无限,但如果我们想要这样做,标准应该引入一个命名常量并描述它的行为。

我宁愿解决您的直接问题(想要一个时间值作为无穷大的前哨),而不是试图强制进行溢出检查。当您对所涉及的类型一无所知时,溢出检查可能会变得非常昂贵(在复杂性和运行时方面),但检查幻数常量(例如chrono::duration<rep, period>::max()chrono::time_point<clock, duration>::max())应该很便宜。

看起来未来的更新(ABI不兼容)将对<thread>进行重大更改,因此它不会再在sleep_for()中溢出,但它仍然受到Windows API支持的限制。类似NtWaitForSingleObject()的代码确实支持64位值,但有符号,因为它同时支持相对(负)和绝对(正)时间。

1指的是30.2.4[thread.req.timing]适用的任何函数,例如this_thread::sleep_for()this_thread::sleep_until(),但也包括timed_mutexrecursive_timed_mutexcondition_variable等中的内容。

相关文章