我可以传递给 std::thread::sleep_for() 和 sleep_until() 的最大值是多少?

What is the maximum value I can pass to std::thread::sleep_for() and sleep_until()?

This question on sleeper forever 有一个答案提到了这个:

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) 中描述为:"The value returned shall compare greater than zero()",完全没有帮助。

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

我可以传递给那些具有明确行为的函数的最高值是多少?

从技术上讲 std::chrono::system_clock::durat‌​ion::max() 应该睡很长时间(比你或你的孙子们活得更久)。标准强制执行了这一点。

但实际上,实施者仍在学习如何处理由 chrono 不同精度持续时间之间的转换引起的溢出。所以错误很常见。

9'000h(一年多一点)可能更实际。这不可能导致溢出。对于您的应用来说,它肯定是“永远”的。

但是,不要犹豫,向您的供应商发送错误报告,抱怨 std::chrono::system_clock::durat‌​ion::max() 不起作用。这应该。让它正常工作很棘手。让它工作是不可移植的,所以要求你写一些包装器来做它是不合理的。


受到下面isanae的精彩评论的启发,该评论要求参考:

30.3.3 [thread.thread.this]/p7 描述 sleep_for 说:

Effects: Blocks the calling thread for the relative timeout (30.2.4) specified by rel_time.

30.2.4 [thread.req.timing] 是线程支持库中所有时序要求的规范,说:

2 Implementations necessarily have some delay in returning from a timeout. Any overhead in interrupt response, function return, and scheduling induces a “quality of implementation” delay, expressed as duration Di. Ideally, this delay would be zero. Further, any contention for processor and memory resources induces a “quality of management” delay, expressed as duration Dm. The delay durations may vary from timeout to timeout, but in all cases shorter is better.

3 The member functions whose names end in _for take an argument that specifies a duration. These functions produce relative timeouts. Implementations should use a steady clock to measure time for these functions.330 Given a duration argument Dt, the real-time duration of the timeout is Dt + Di + Dm .

好的,现在我被逗乐了,因为我们不是在谈论成员函数。我们谈论的是命名空间作用域函数。这是一个缺陷。随意 submit one.

但是规范没有提供溢出的宽限期。规范(几乎)清楚地表明,实现不能 return 直到 指定的延迟之后。 多少之后是模糊的,但清楚的是它不能return之前。

如果您“窃听”STL 而他不合作,请将他转介给我,我们会解决的。 :-) 也许有一个我没有看到的标准错误,应该修复。如果是这样,我可以帮助您针对标准而不是针对 VS 提交错误。或者也许 VS 已经解决了这个问题,并且可以在升级中修复。

如果这是 VS 中的错误,请告知 STL,我非常乐意协助修复它。在不同的平台上解决这个问题有不同的权衡。

目前,我不能发誓在我自己的实现 (libc++) 中没有这个 class 的错误。所以这里没有高马。 std::lib 很难做到这一点。

更新

我看过 libc++ sleep_forsleep_untilsleep_for 通过休眠“长时间”正确处理溢出(OS 可以处理)。 sleep_until 存在溢出错误。

这是一个经过简单测试的固定 sleep_until:

template <class _Clock, class _Duration>
void
sleep_until(const chrono::time_point<_Clock, _Duration>& __t)
{
    using namespace chrono;
    using __ldsec = duration<long double>;
    _LIBCPP_CONSTEXPR time_point<_Clock, __ldsec> _Max =
                          time_point<_Clock, nanoseconds>::max();
    time_point<_Clock, nanoseconds> __ns;
    if (__t < _Max)
    {
        __ns = time_point_cast<nanoseconds>(__t);
        if (__ns < __t)
            __ns += nanoseconds{1};
    }
    else
        __ns = time_point<_Clock, nanoseconds>::max();
    mutex __mut;
    condition_variable __cv;
    unique_lock<mutex> __lk(__mut);
    while (_Clock::now() < __ns)
        __cv.wait_until(__lk, __ns);
}

基本策略是使用 long double 表示法进行溢出检查,该表示法不仅具有非常大的最大可表示值,而且还使用饱和算法(具有无穷大)。如果输入值太大 OS 无法处理,请将其截断为 OS 可以处理的值。

在某些平台上,可能不希望求助于浮点运算。人们可能会使用 __int128_t 来代替。或者有一个更复杂的技巧,即在进行比较之前转换为输入和本机持续时间的“最小公倍数”。该转换仅涉及除法(不涉及乘法),因此不会溢出。然而,对于几乎相等的两个值,它并不总是给出准确的答案。但对于这个用例来说它应该工作得很好。

对于那些对后一种 (lcm) 策略感兴趣的人,这里是计算该类型的方法:

namespace detail
{

template <class Duration0, class ...Durations>
struct lcm_type;

template <class Duration>
struct lcm_type<Duration>
{
    using type = Duration;
};

template <class Duration1, class Duration2>
struct lcm_type<Duration1, Duration2>
{
    template <class D>
    using invert = std::chrono::duration
                   <
                       typename D::rep,
                       std::ratio_divide<std::ratio<1>, typename D::period>
                   >;

    using type = invert<typename std::common_type<invert<Duration1>,
                                                  invert<Duration2>>::type>;
};

template <class Duration0, class Duration1, class Duration2, class ...Durations>
struct lcm_type<Duration0, Duration1, Duration2, Durations...>
{
    using type = typename lcm_type<
                     typename lcm_type<Duration0, Duration1>::type,
                     Duration2, Durations...>::type;
};

}  // namespace detail

可以认为 lcm_type<duration1, duration2>common_type<duration1, duration2> 的对立面。前者找到转换为仅划分的持续时间。后者找到一个持续时间,转换为仅相乘。

未指定,会溢出

我与 Visual C++ 标准库开发人员之一的 Billy O'Neal 和 libc++ 的主要作者 Howard Hinnant 进行了讨论。我的结论是 threading 库中的 _for_until 系列将以未指定的方式溢出 ,您不应该尝试将较大的值传递给它们。我不清楚该标准是否在该主题上未指定。

问题

所有定时函数1 采用 durationtime_point。两者都由它们的基础类型 (representation) 和比率 (period) 定义。周期也可以被认为是一个“单位”,例如秒或纳秒。

溢出主要发生在两个地方:

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

通话前

在这种情况下可以避免溢出,就像霍华德在他的回答中提到的那样,但是“实现者仍在学习如何处理由 chrono 不同精度持续时间之间的转换引起的溢出”。

例如,Visual C++ 2017 通过将给定持续时间添加到返回的当前时间来根据 sleep_until() 实现 sleep_for() 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_until() 方面对 sleep_for() 的实现。我认为在调用特定于平台的函数之前,您提供的任何值都应该在最终转换之前保持不变。

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

Visual C++

我从 Billy O'Neal 收到的电子邮件中引用了一些内容,因为它们增加了标准库开发人员的观点:

Are you saying that this:

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

is undefined behaviour by the standard?

据我所知,是的。这是一个灰色区域——没有真正为这些函数指定最大允许范围,但考虑到它们接受任意 time_point/duration 的性质,这可能由某些用户提供的 bignum 类型支持标准库不知道,转​​换为某些基础 time_point/duration 类型基本上是强制性的。 <chrono> 的设计将处理溢出视为非目标(参见 duration_cast,例如,它完全禁止实现“好像无穷大”和类似的东西)。

标准 [...] 没有给我们任何方式来报告转换失败——行为实际上是未定义的。我们被明确禁止抛出异常,我们无法推理如果你超过 LLONG_MAX 会发生什么,所以我们唯一可能的反应是“好像无穷大”或直接转到 std::terminate(),不要pass go,不收$200

libstdc++ 和 libc++ 的目标平台 system_clock 实际上映射到平台理解的东西,其中 Unix 时间戳是土地法则。我们不针对这样的平台,有义务映射to/from“DWORD毫秒”and/orFILETIME.

我唯一能想到的可能是这个东西的合理用例是有某种标记值,意思是“无限”,但如果我们想去那里,标准应该引入一个命名的常数并描述其行为。

我宁愿解决您的直接问题(希望时间值成为无穷大的哨兵),也不愿尝试强制进行溢出检查。当您对所涉及的类型一无所知时进行溢出检查可能会变得非常昂贵(在复杂性和 运行 时间上),但检查魔术常量(例如 chrono::duration<rep, period>::max()chrono::time_point<clock, duration>::max())应该便宜。

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

1 “定时函数”,我指的是 30.2.4 [thread.req.timing]适用,例如this_thread::sleep_for()this_thread::sleep_until(),但也适用于timed_mutexrecursive_timed_mutexcondition_variable