authorgravatar for cryptocode@zolo.iocryptocode <cryptocode@zolo.io> 2022-04-08 20:26:56+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-04-08 13:26:56-05:00
log531d5b213f5cc7450c56bea3fc2050dda5794067
treeba2ab7d1e4362d3642dab55ec9c05a7c7fccbdf0
parent6ae8fe193bd5397b9fceea98ba684d9d870286cf
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

std: add Thread.Condition.timedWait (#11352)

* std: add Thread.Condition.timedWait I needed the equivalent of `std::condition_variable::wait_for`, but it's missing in std. This PR adds an implementation, following the status quo of using std.os.CLOCK.REALTIME in the pthread case (i.e. Futex) A follow-up patch moving futex/condition stuff to monotonic clocks where available seems like a good idea. This would involve conditionally exposing more functions and constants through std.c and std.os. For instance, Chromium picks `pthread_cond_timedwait_relative_np` on macOS and `clock_gettime(CLOCK_MONOTONIC...)` on BSD's. Tested on Windows 11, macOS 12.2.1 and Linux (with/without libc) * Sleep in the single threaded case, handle timeout overflow in the Windows case and address a race condition in the AtomicCondition case.

1 files changed, 173 insertions(+), 2 deletions(-)

lib/std/Thread/Condition.zig+173-2
......@@ -17,6 +17,10 @@ pub fn wait(cond: *Condition, mutex: *Mutex) void {
1717 cond.impl.wait(mutex);
1818}
1919
20pub fn timedWait(cond: *Condition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {
21 try cond.impl.timedWait(mutex, timeout_ns);
22}
23
2024pub fn signal(cond: *Condition) void {
2125 cond.impl.signal();
2226}
......@@ -41,6 +45,14 @@ pub const SingleThreadedCondition = struct {
4145 unreachable; // deadlock detected
4246 }
4347
48 pub fn timedWait(cond: *SingleThreadedCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {
49 _ = cond;
50 _ = mutex;
51 _ = timeout_ns;
52 std.time.sleep(timeout_ns);
53 return error.TimedOut;
54 }
55
4456 pub fn signal(cond: *SingleThreadedCondition) void {
4557 _ = cond;
4658 }
......@@ -63,6 +75,25 @@ pub const WindowsCondition = struct {
6375 assert(rc != windows.FALSE);
6476 }
6577
78 pub fn timedWait(cond: *WindowsCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {
79 var timeout_checked = std.math.cast(windows.DWORD, timeout_ns / std.time.ns_per_ms) catch overflow: {
80 break :overflow std.math.maxInt(windows.DWORD);
81 };
82
83 // Handle the case where timeout is INFINITE, otherwise SleepConditionVariableSRW's time-out never elapses
84 const timeout_overflowed = timeout_checked == windows.INFINITE;
85 timeout_checked -= @boolToInt(timeout_overflowed);
86
87 const rc = windows.kernel32.SleepConditionVariableSRW(
88 &cond.cond,
89 &mutex.impl.srwlock,
90 timeout_checked,
91 @as(windows.ULONG, 0),
92 );
93 if (rc == windows.FALSE and windows.kernel32.GetLastError() == windows.Win32Error.TIMEOUT) return error.TimedOut;
94 assert(rc != windows.FALSE);
95 }
96
6697 pub fn signal(cond: *WindowsCondition) void {
6798 windows.kernel32.WakeConditionVariable(&cond.cond);
6899 }
......@@ -80,6 +111,24 @@ pub const PthreadCondition = struct {
80111 assert(rc == .SUCCESS);
81112 }
82113
114 pub fn timedWait(cond: *PthreadCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {
115 var ts: std.os.timespec = undefined;
116 std.os.clock_gettime(std.os.CLOCK.REALTIME, &ts) catch unreachable;
117 ts.tv_sec += @intCast(@TypeOf(ts.tv_sec), timeout_ns / std.time.ns_per_s);
118 ts.tv_nsec += @intCast(@TypeOf(ts.tv_nsec), timeout_ns % std.time.ns_per_s);
119 if (ts.tv_nsec >= std.time.ns_per_s) {
120 ts.tv_sec += 1;
121 ts.tv_nsec -= std.time.ns_per_s;
122 }
123
124 const rc = std.c.pthread_cond_timedwait(&cond.cond, &mutex.impl.pthread_mutex, &ts);
125 return switch (rc) {
126 .SUCCESS => {},
127 .TIMEDOUT => error.TimedOut,
128 else => unreachable,
129 };
130 }
131
83132 pub fn signal(cond: *PthreadCondition) void {
84133 const rc = std.c.pthread_cond_signal(&cond.cond);
85134 assert(rc == .SUCCESS);
......@@ -100,6 +149,7 @@ pub const AtomicCondition = struct {
100149
101150 pub const QueueItem = struct {
102151 futex: i32 = 0,
152 dequeued: bool = false,
103153
104154 fn wait(cond: *@This()) void {
105155 while (@atomicLoad(i32, &cond.futex, .Acquire) == 0) {
......@@ -122,6 +172,39 @@ pub const AtomicCondition = struct {
122172 }
123173 }
124174
175 pub fn timedWait(cond: *@This(), timeout_ns: u64) error{TimedOut}!void {
176 const start_time = std.time.nanoTimestamp();
177 while (@atomicLoad(i32, &cond.futex, .Acquire) == 0) {
178 switch (builtin.os.tag) {
179 .linux => {
180 var ts: std.os.timespec = undefined;
181 ts.tv_sec = @intCast(@TypeOf(ts.tv_sec), timeout_ns / std.time.ns_per_s);
182 ts.tv_nsec = @intCast(@TypeOf(ts.tv_nsec), timeout_ns % std.time.ns_per_s);
183 switch (linux.getErrno(linux.futex_wait(
184 &cond.futex,
185 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT,
186 0,
187 &ts,
188 ))) {
189 .SUCCESS => {},
190 .INTR => {},
191 .AGAIN => {},
192 .TIMEDOUT => return error.TimedOut,
193 .INVAL => {}, // possibly timeout overflow
194 .FAULT => unreachable,
195 else => unreachable,
196 }
197 },
198 else => {
199 if (std.time.nanoTimestamp() - start_time >= timeout_ns) {
200 return error.TimedOut;
201 }
202 std.atomic.spinLoopHint();
203 },
204 }
205 }
206 }
207
125208 fn notify(cond: *@This()) void {
126209 @atomicStore(i32, &cond.futex, 1, .Release);
127210
......@@ -158,6 +241,41 @@ pub const AtomicCondition = struct {
158241 mutex.lock();
159242 }
160243
244 pub fn timedWait(cond: *AtomicCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {
245 var waiter = QueueList.Node{ .data = .{} };
246
247 {
248 cond.queue_mutex.lock();
249 defer cond.queue_mutex.unlock();
250
251 cond.queue_list.prepend(&waiter);
252 @atomicStore(bool, &cond.pending, true, .SeqCst);
253 }
254
255 var timed_out = false;
256 mutex.unlock();
257 defer mutex.lock();
258 waiter.data.timedWait(timeout_ns) catch |err| switch (err) {
259 error.TimedOut => {
260 defer if (!timed_out) {
261 waiter.data.wait();
262 };
263 cond.queue_mutex.lock();
264 defer cond.queue_mutex.unlock();
265
266 if (!waiter.data.dequeued) {
267 timed_out = true;
268 cond.queue_list.remove(&waiter);
269 }
270 },
271 else => unreachable,
272 };
273
274 if (timed_out) {
275 return error.TimedOut;
276 }
277 }
278
161279 pub fn signal(cond: *AtomicCondition) void {
162280 if (@atomicLoad(bool, &cond.pending, .SeqCst) == false)
163281 return;
......@@ -167,12 +285,16 @@ pub const AtomicCondition = struct {
167285 defer cond.queue_mutex.unlock();
168286
169287 const maybe_waiter = cond.queue_list.popFirst();
288 if (maybe_waiter) |waiter| {
289 waiter.data.dequeued = true;
290 }
170291 @atomicStore(bool, &cond.pending, cond.queue_list.first != null, .SeqCst);
171292 break :blk maybe_waiter;
172293 };
173294
174 if (maybe_waiter) |waiter|
295 if (maybe_waiter) |waiter| {
175296 waiter.data.notify();
297 }
176298 }
177299
178300 pub fn broadcast(cond: *AtomicCondition) void {
......@@ -186,12 +308,19 @@ pub const AtomicCondition = struct {
186308 defer cond.queue_mutex.unlock();
187309
188310 const waiters = cond.queue_list;
311
312 var it = waiters.first;
313 while (it) |node| : (it = node.next) {
314 node.data.dequeued = true;
315 }
316
189317 cond.queue_list = .{};
190318 break :blk waiters;
191319 };
192320
193 while (waiters.popFirst()) |waiter|
321 while (waiters.popFirst()) |waiter| {
194322 waiter.data.notify();
323 }
195324 }
196325};
197326
......@@ -238,3 +367,45 @@ test "Thread.Condition" {
238367
239368 for (threads) |t| t.join();
240369}
370
371test "Thread.Condition.timedWait" {
372 if (builtin.single_threaded) {
373 return error.SkipZigTest;
374 }
375
376 var cond = Condition{};
377 var mut = Mutex{};
378
379 // Expect a timeout, as the condition variable is never signaled
380 {
381 mut.lock();
382 defer mut.unlock();
383 try testing.expectError(error.TimedOut, cond.timedWait(&mut, 10 * std.time.ns_per_ms));
384 }
385
386 // Expect a signal before timeout
387 {
388 const TestContext = struct {
389 cond: *Condition,
390 mutex: *Mutex,
391 n: *u32,
392 fn worker(ctx: *@This()) void {
393 ctx.mutex.lock();
394 defer ctx.mutex.unlock();
395 ctx.n.* = 1;
396 ctx.cond.signal();
397 }
398 };
399
400 var n: u32 = 0;
401
402 var ctx = TestContext{ .cond = &cond, .mutex = &mut, .n = &n };
403 mut.lock();
404 var thread = try std.Thread.spawn(.{}, TestContext.worker, .{&ctx});
405 // Looped check to handle spurious wakeups
406 while (n != 1) try cond.timedWait(&mut, 500 * std.time.ns_per_ms);
407 mut.unlock();
408 try testing.expect(n == 1);
409 thread.join();
410 }
411}