Opened 7 years ago

Closed 7 years ago

#11331 closed Bugs (wontfix)

boost::this_thread::sleep_for does not sleep for requested time

Reported by: Scott Minor <scott.minor@…> Owned by: Niall Douglas
Milestone: To Be Determined Component: thread
Version: Boost 1.58.0 Severity: Problem
Keywords: sleep_for Cc:

Description

boost::this_thread::sleep_for does not accurately sleep for the requested amount of time (+/- 30ms) on a Windows 7 x64 environment when the requested time is on the order of half a minute. std::this_thread::sleep_for is able to accurately sleep for the requested amount of time. Here is a sample program illustrating the issue:

#include <boost/thread.hpp>
#include <boost/chrono.hpp>

#include <chrono>
#include <iostream>
#include <thread>

void boostThreadFunction ()
{
   std::cout << "Starting Boost thread" << std::endl;
   for (int i = 0; i < 10; ++i)
   {
      auto sleep_time = boost::chrono::milliseconds {29000 + 100 * i};
      auto mark = std::chrono::steady_clock::now ();
      boost::this_thread::sleep_for (sleep_time);
      auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
         std::chrono::steady_clock::now () - mark);
      std::cout << "Boost thread:" << std::endl;
      std::cout << "\tSupposed to sleep for:\t" << sleep_time.count () << " ms" << std::endl;
      std::cout << "\tActually slept for:\t" << duration.count () << " ms" << std::endl << std::endl;
   }
}

void stdThreadFunction ()
{
   std::cout << "Starting Std thread" << std::endl;
   for (int i = 0; i < 10; ++i)
   {
      auto sleep_time = std::chrono::milliseconds {29000 + 100 * i};
      auto mark = std::chrono::steady_clock::now ();
      std::this_thread::sleep_for (sleep_time);
      auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
         std::chrono::steady_clock::now () - mark);
      std::cout << "Std thread:" << std::endl;
      std::cout << "\tSupposed to sleep for:\t" << sleep_time.count () << " ms" << std::endl;
      std::cout << "\tActually slept for:\t" << duration.count () << " ms" << std::endl << std::endl;
   }
}

int main ()
{
   boost::thread boost_thread (&boostThreadFunction);
   std::this_thread::sleep_for (std::chrono::seconds (10));
   std::thread std_thread (&stdThreadFunction);
   boost_thread.join ();
   std_thread.join ();
   system ("pause");
   return 0;
}

Here is the output produced in my environment:

Starting Boost thread
Starting Std thread
Boost thread:
        Supposed to sleep for:  29000 ms
        Actually slept for:     29690 ms

Std thread:
        Supposed to sleep for:  29000 ms
        Actually slept for:     29009 ms

Boost thread:
        Supposed to sleep for:  29100 ms
        Actually slept for:     29999 ms

Std thread:
        Supposed to sleep for:  29100 ms
        Actually slept for:     29111 ms

Boost thread:
        Supposed to sleep for:  29200 ms
        Actually slept for:     29990 ms

Std thread:
        Supposed to sleep for:  29200 ms
        Actually slept for:     29172 ms

Boost thread:
        Supposed to sleep for:  29300 ms
        Actually slept for:     30005 ms

Std thread:
        Supposed to sleep for:  29300 ms
        Actually slept for:     29339 ms

Boost thread:
        Supposed to sleep for:  29400 ms
        Actually slept for:     30003 ms

Std thread:
        Supposed to sleep for:  29400 ms
        Actually slept for:     29405 ms

Boost thread:
        Supposed to sleep for:  29500 ms
        Actually slept for:     29999 ms

Std thread:
        Supposed to sleep for:  29500 ms
        Actually slept for:     29472 ms

Boost thread:
        Supposed to sleep for:  29600 ms
        Actually slept for:     29999 ms

Std thread:
        Supposed to sleep for:  29600 ms
        Actually slept for:     29645 ms

Boost thread:
        Supposed to sleep for:  29700 ms
        Actually slept for:     29998 ms

Std thread:
        Supposed to sleep for:  29700 ms
        Actually slept for:     29706 ms

Boost thread:
        Supposed to sleep for:  29800 ms
        Actually slept for:     29998 ms

Std thread:
        Supposed to sleep for:  29800 ms
        Actually slept for:     29807 ms

Boost thread:
        Supposed to sleep for:  29900 ms
        Actually slept for:     30014 ms

Std thread:
        Supposed to sleep for:  29900 ms
        Actually slept for:     29915 ms

Looking at libs/thread/src/win32/thread.cpp, this inaccuracy may be by design; a +/-5% tolerance appears to be added to the wait time to take advantage of coalescing timers. See related StackOverflow question: http://stackoverflow.com/questions/30381866/boost-thread-wakes-up-too-late-in-1-58

However, we are dependent on sleep_for() being as accurate as possible; for us, this is a breaking change.

Change History (8)

comment:1 by viboes, 7 years ago

Owner: changed from Anthony Williams to Niall Douglas

Niall, I believe that you did this changes. Could you recall us why this was necessary?

in reply to:  1 comment:2 by Niall Douglas, 7 years ago

Replying to viboes:

Niall, I believe that you did this changes. Could you recall us why this was necessary?

Yep, this change was all me.

The change was not my design however. boost-dev was asked for options, and this design was the consensus that emerged. I personally didn't agree with it, but there you go. I might add that just because we say that we don't mind a +/- 1 sec tolerance doesn't mean that Windows actually gives us that, it just /may/ give us up to that.

A clamp on the upper bound of the tolerance was considered unnecessary precisely because the correct wakeup is specified to Windows, and whatever the next interrupt is will execute the timer. The tolerance is not automatic lateness, it's simply permission to increase variance past the system tick rate of ~30ms. That has huge benefits for battery operated devices, and it also helps catch buggy code like the OPs.

Regarding this statement:

Replying to Scott Minor <scott.minor@…>:

However, we are dependent on sleep_for() being as accurate as possible; for us, this is a breaking change.

No, it is your algorithm which is broken. If your use depends on any accuracy at all from any STL11 timed wait implementation, your code is wrong and needs to be fixed. The C++ standard is very clear than you can expect no guarantees whatsoever in any timed sleep function (including any amount of actual sleep), and any code which is written to require accuracy is incorrect. I might add that the Microsoft STL may be likely to gain a similar timer design to Boost.Thread soon as I coordinated with Stephan what I was about to change in Boost.Thread. We're just earlier to ship, that's all.

Now, that said, I personally speaking would just love if we could add timer hardness/softness support to the STL and very considerably strengthen those guarantees on platforms allowing it, but that's probably work best done in Boost.Thread v5 (and the associated changes in Boost.Chrono) as an early design imperative so the rewrite is timer hardness/softness friendly from the very beginning.

Niall

comment:3 by scott.minor.13@…, 7 years ago

Thanks for the follow-up. How would you recommend users of Boost 1.58 rewrite their algorithms if they are impacted by this behavior?

in reply to:  3 comment:4 by Niall Douglas, 7 years ago

Replying to scott.minor.13@…:

Thanks for the follow-up. How would you recommend users of Boost 1.58 rewrite their algorithms if they are impacted by this behavior?

Depends on the algorithm. 95% of the time if you need to wait for exactly 30 seconds down to the millisecond, there is something very wrong with your assumptions because that is only ever somewhat likely on a non-realtime OS, or any system able to swap memory onto disc.

For example, if you actually merely need to poll something every thirty seconds, what you do is to adjust subsequent timeouts for when some timeout is late, and you aim for 120 wakeups per hour, not a sleep of 30 seconds each. You obviously need to adjust readings according to wakeup error. This is the kind of thing engineers ought to do from the beginning, but it's just hugely easier not to.

Another strategy is you sleep 80% of 30 seconds, then enter a yield loop for another 10%, then a spin loop for the remaining 10% - this delivers microsecond accurate sleeps most of the time. For any occasions where the OS context switches at exactly the wrong moment you still need a method of detecting the error and throwing away/retrying the exact wait.

Niall

comment:5 by viboes, 7 years ago

Could we close the issue as works as expected?

comment:6 by viboes, 7 years ago

Niall, why do the boost dev list wanted the change? Have you a pointer, please? What issue was this change fixing?

in reply to:  6 comment:7 by Niall Douglas, 7 years ago

Replying to viboes:

Niall, why do the boost dev list wanted the change? Have you a pointer, please? What issue was this change fixing?

I believe the discussion began with #9856 which contains links to the appropriate boost-dev threads.

Niall

comment:8 by viboes, 7 years ago

Resolution: wontfix
Status: newclosed
Note: See TracTickets for help on using tickets.