Skip to content

Commit d0f42ca

Browse files
committed
[Threading][Win32] Fix the wait functions for Win32 to always over-wait.
Rather than bodging the test to make it more robust, fix the functions in the Threading layer to behave the same as they do on other platforms, i.e. to guarantee that they always wait *at least* the amount of time you asked for. rdar://100236038
1 parent 33e74cd commit d0f42ca

File tree

2 files changed

+69
-40
lines changed

2 files changed

+69
-40
lines changed

include/swift/Threading/Impl/Win32.h

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,76 @@ template <class Rep, class Period>
122122
inline bool cond_wait(cond_handle &handle,
123123
std::chrono::duration<Rep, Period> duration) {
124124
auto ms = chrono_utils::ceil<std::chrono::milliseconds>(duration);
125+
126+
/* If you are paying attention to the next line, you are now asking
127+
128+
"Why are you adding 16 - that seems mad?"
129+
130+
To explain, we need to understand how the Sleep...() APIs in
131+
Windows work.
132+
133+
The Windows kernel runs a timer, the system tick, which is
134+
used for scheduling; the timer in question, for historical
135+
reasons, by default runs at 64Hz. Every time the timer fires,
136+
Windows updates the tick count, schedules processes and so on.
137+
138+
When you ask to sleep, there are two cases:
139+
140+
1. You've asked to sleep for less than a tick, or
141+
142+
2. You've asked to sleep for at least a tick.
143+
144+
In case 1, the sleep functions appear to run a delay loop; this
145+
is by its very nature inaccurate and you may or may not wait less
146+
than the time you requested. You might also get rescheduled,
147+
in which case you'll wait at least a whole tick.
148+
149+
In case 2, Windows appears to be adding the requested wait to
150+
its current tick count to work out when to wake your thread.
151+
That *sounds* sensible, until you realise that if it does this
152+
towards the end of a tick period, you will wait for that much
153+
less time. i.e. the sleep functions will return *early* by
154+
up to one tick period.
155+
156+
This is especially unfortunate if you ask to wait for exactly
157+
one tick period, because you will end up waiting for anything
158+
between no time at all and a full tick period.
159+
160+
To complicate matters, the system tick rate might not be 64Hz;
161+
the multimedia timer API can cause it to change to as high as
162+
1kHz, and on *some* machines this happens globally for all
163+
processes, while on *other* machines the kernel is actually using
164+
a separate system tick rate and merely pretending to Win32
165+
processes that things still work this way.
166+
167+
On other platforms, these kinds of functions are guaranteed to
168+
wait for at least the time you requested, and we'd like for that
169+
to be true for the Threading package's APIs here. One way to
170+
achieve that is to add a whole tick's worth of milliseconds
171+
to the time we're requesting to wait for. In that case, we'll
172+
avoid the delay loop from case (1), and we'll also guarantee that
173+
we wait for at least the amount of time the caller of this
174+
function expected.
175+
176+
It would be nice if there were a Windows API we could call to
177+
obtain the current system tick period. The Internet suggests
178+
that the undocumented call NtQueryTimerResolution() might be of
179+
some use for this, but it turns out that that just gives you
180+
the kernel's idea of the system tick period, which isn't the
181+
same as Win32's idea necessarily. e.g. on my test system, I
182+
can see that the tick period is 15.625ms, while that call tells
183+
me 1ms.
184+
185+
We don't want to change the system tick rate using the multimedia
186+
timer API mentioned earlier, because on some machines that is
187+
global and will use extra power and CPU cycles.
188+
189+
The upshot is that the best choice here appears to be to add
190+
15.625ms (1/64s) to the time we're asking to wait. That rounds
191+
up to 16ms, which is why there's a +16. */
125192
return SleepConditionVariableSRW(&handle.condition,
126193
&handle.lock,
127-
DWORD(ms.count()),
194+
DWORD(ms.count()) + 16,
128195
0);
129196
}
130197
inline bool cond_wait(cond_handle &handle,

unittests/Threading/ConditionVariable.cpp

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,13 @@
1919
#include "ThreadingHelpers.h"
2020
#include "LockingHelpers.h"
2121

22-
#ifdef _WIN32
23-
#include <windows.h>
24-
#endif
25-
2622
using namespace swift;
2723

2824
template <typename Body>
2925
std::chrono::duration<double> measureDuration(Body body) {
30-
#ifdef _WIN32
31-
using namespace std::chrono_literals;
32-
33-
/* On Windows, we use SleepConditionVariableSRW() to implement
34-
ConditionVariable. The way Windows implements the SleepXXX() functions,
35-
if you specify a timeout smaller than the current system tick rate,
36-
the function may return early. See
37-
38-
https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleep
39-
40-
This causes flakiness in this test, which is undesirable.
41-
42-
To combat this, we adjust the system tick rate temporarily here.
43-
44-
Even *after* doing that, measurement suggests that Windows can return
45-
early by an amount not exceeding a system tick. */
46-
TIMECAPS tc;
47-
UINT uTimerRes;
48-
49-
ASSERT_NE(timeGetDevCaps(&tc, sizeof(TIMECAPS)), TIMERR_NOERROR);
50-
51-
uTimerRes = min(max(tc.wPeriodMin, 1/*ms*/), tc.wPeriodMax);
52-
timeBeginPeriod(uTimerRes);
53-
#endif
54-
5526
auto start = std::chrono::steady_clock::now();
5627
body();
57-
std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start;
58-
59-
#ifdef _WIN32
60-
timeEndPeriod(uTimerRes);
61-
62-
// Lie about the elapsed time to make the test more robust
63-
elapsed += 0.001s;
64-
#endif
65-
66-
return elapsed;
28+
return std::chrono::steady_clock::now() - start;
6729
}
6830

6931
// -----------------------------------------------------------------------------

0 commit comments

Comments
 (0)