authorgravatar for 45520026+kprotty@users.noreply.github.comprotty <45520026+kprotty@users.noreply.github.com> 2022-04-23 19:35:56-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2022-04-23 19:35:56-05:00
log963ac60918b39cafdda3cb99eff4cd9d20edd839
tree8b480e6c1c01c11758f47b1126c53e75a339d1a1
parentdaef82d06fd6b30d2cab7f5a6723cf2e3c7b48c6
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

std.Thread: Mutex and Condition improvements (#11497)

* Thread: minor cleanups * Thread: rewrite Mutex * Thread: introduce Futex.Deadline * Thread: Condition rewrite + cleanup * Mutex: optimize lock fast path * Condition: more docs * Thread: more mutex + condition docs * Thread: remove broken Condition test * Thread: zig fmt * address review comments + fix Thread.DummyMutex in GPA * Atomic: disable bitRmw x86 inline asm for stage2 * GPA: typo mutex_init * Thread: remove noalias on stuff * Thread: comment typos + clarifications

7 files changed, 810 insertions(+), 640 deletions(-)

lib/std/Thread.zig+1-26
...@@ -459,9 +459,8 @@ const UnsupportedImpl = struct {...@@ -459,9 +459,8 @@ const UnsupportedImpl = struct {
459 }459 }
460460
461 fn unsupported(unusued: anytype) noreturn {461 fn unsupported(unusued: anytype) noreturn {
462 @compileLog("Unsupported operating system", target.os.tag);
463 _ = unusued;462 _ = unusued;
464 unreachable;463 @compileError("Unsupported operating system " ++ @tagName(target.os.tag));
465 }464 }
466};465};
467466
...@@ -1188,27 +1187,3 @@ test "Thread.detach" {...@@ -1188,27 +1187,3 @@ test "Thread.detach" {
1188 event.wait();1187 event.wait();
1189 try std.testing.expectEqual(value, 1);1188 try std.testing.expectEqual(value, 1);
1190}1189}
1191
1192fn testWaitForSignal(mutex: *Mutex, cond: *Condition) void {
1193 mutex.lock();
1194 defer mutex.unlock();
1195 cond.signal();
1196 cond.wait(mutex);
1197}
1198
1199test "Condition.signal" {
1200 if (builtin.single_threaded) return error.SkipZigTest;
1201
1202 var mutex = Mutex{};
1203 var cond = Condition{};
1204
1205 var thread: Thread = undefined;
1206 {
1207 mutex.lock();
1208 defer mutex.unlock();
1209 thread = try Thread.spawn(.{}, testWaitForSignal, .{ &mutex, &cond });
1210 cond.wait(&mutex);
1211 cond.signal();
1212 }
1213 thread.join();
1214}
lib/std/Thread/Condition.zig+435-308
...@@ -1,411 +1,538 @@...@@ -1,411 +1,538 @@
1//! A condition provides a way for a kernel thread to block until it is signaled1//! Condition variables are used with a Mutex to efficiently wait for an arbitrary condition to occur.
2//! to wake up. Spurious wakeups are possible.2//! It does this by atomically unlocking the mutex, blocking the thread until notified, and finally re-locking the mutex.
3//! This API supports static initialization and does not require deinitialization.3//! Condition can be statically initialized and is at most `@sizeOf(u64)` large.
44//!
5impl: Impl = .{},5//! Example:
6//! ```
7//! var m = Mutex{};
8//! var c = Condition{};
9//! var predicate = false;
10//!
11//! fn consumer() void {
12//! m.lock();
13//! defer m.unlock();
14//!
15//! while (!predicate) {
16//! c.wait(&mutex);
17//! }
18//! }
19//!
20//! fn producer() void {
21//! m.lock();
22//! defer m.unlock();
23//!
24//! predicate = true;
25//! c.signal();
26//! }
27//!
28//! const thread = try std.Thread.spawn(.{}, producer, .{});
29//! consumer();
30//! thread.join();
31//! ```
32//!
33//! Note that condition variables can only reliably unblock threads that are sequenced before them using the same Mutex.
34//! This means that the following is allowed to deadlock:
35//! ```
36//! thread-1: mutex.lock()
37//! thread-1: condition.wait(&mutex)
38//!
39//! thread-2: // mutex.lock() (without this, the following signal may not see the waiting thread-1)
40//! thread-2: // mutex.unlock() (this is optional for correctness once locked above, as signal can be called without holding the mutex)
41//! thread-2: condition.signal()
42//! ```
643
7const std = @import("../std.zig");44const std = @import("../std.zig");
8const builtin = @import("builtin");45const builtin = @import("builtin");
9const Condition = @This();46const Condition = @This();
10const windows = std.os.windows;
11const linux = std.os.linux;
12const Mutex = std.Thread.Mutex;47const Mutex = std.Thread.Mutex;
48
49const os = std.os;
13const assert = std.debug.assert;50const assert = std.debug.assert;
14const testing = std.testing;51const testing = std.testing;
52const Atomic = std.atomic.Atomic;
53const Futex = std.Thread.Futex;
54
55impl: Impl = .{},
1556
16pub fn wait(cond: *Condition, mutex: *Mutex) void {57/// Atomically releases the Mutex, blocks the caller thread, then re-acquires the Mutex on return.
17 cond.impl.wait(mutex);58/// "Atomically" here refers to accesses done on the Condition after acquiring the Mutex.
59///
60/// The Mutex must be locked by the caller's thread when this function is called.
61/// A Mutex can have multiple Conditions waiting with it concurrently, but not the opposite.
62/// It is undefined behavior for multiple threads to wait ith different mutexes using the same Condition concurrently.
63/// Once threads have finished waiting with one Mutex, the Condition can be used to wait with another Mutex.
64///
65/// A blocking call to wait() is unblocked from one of the following conditions:
66/// - a spurious ("at random") wake up occurs
67/// - a future call to `signal()` or `broadcast()` which has acquired the Mutex and is sequenced after this `wait()`.
68///
69/// Given wait() can be interrupted spuriously, the blocking condition should be checked continuously
70/// irrespective of any notifications from `signal()` or `broadcast()`.
71pub fn wait(self: *Condition, mutex: *Mutex) void {
72 self.impl.wait(mutex, null) catch |err| switch (err) {
73 error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out
74 };
18}75}
1976
20pub fn timedWait(cond: *Condition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {77/// Atomically releases the Mutex, blocks the caller thread, then re-acquires the Mutex on return.
21 try cond.impl.timedWait(mutex, timeout_ns);78/// "Atomically" here refers to accesses done on the Condition after acquiring the Mutex.
79///
80/// The Mutex must be locked by the caller's thread when this function is called.
81/// A Mutex can have multiple Conditions waiting with it concurrently, but not the opposite.
82/// It is undefined behavior for multiple threads to wait ith different mutexes using the same Condition concurrently.
83/// Once threads have finished waiting with one Mutex, the Condition can be used to wait with another Mutex.
84///
85/// A blocking call to `timedWait()` is unblocked from one of the following conditions:
86/// - a spurious ("at random") wake occurs
87/// - the caller was blocked for around `timeout_ns` nanoseconds, in which `error.Timeout` is returned.
88/// - a future call to `signal()` or `broadcast()` which has acquired the Mutex and is sequenced after this `timedWait()`.
89///
90/// Given `timedWait()` can be interrupted spuriously, the blocking condition should be checked continuously
91/// irrespective of any notifications from `signal()` or `broadcast()`.
92pub fn timedWait(self: *Condition, mutex: *Mutex, timeout_ns: u64) error{Timeout}!void {
93 return self.impl.wait(mutex, timeout_ns);
22}94}
2395
24pub fn signal(cond: *Condition) void {96/// Unblocks at least one thread blocked in a call to `wait()` or `timedWait()` with a given Mutex.
25 cond.impl.signal();97/// The blocked thread must be sequenced before this call with respect to acquiring the same Mutex in order to be observable for unblocking.
98/// `signal()` can be called with or without the relevant Mutex being acquired and have no "effect" if there's no observable blocked threads.
99pub fn signal(self: *Condition) void {
100 self.impl.wake(.one);
26}101}
27102
28pub fn broadcast(cond: *Condition) void {103/// Unblocks all threads currently blocked in a call to `wait()` or `timedWait()` with a given Mutex.
29 cond.impl.broadcast();104/// The blocked threads must be sequenced before this call with respect to acquiring the same Mutex in order to be observable for unblocking.
105/// `broadcast()` can be called with or without the relevant Mutex being acquired and have no "effect" if there's no observable blocked threads.
106pub fn broadcast(self: *Condition) void {
107 self.impl.wake(.all);
30}108}
31109
32const Impl = if (builtin.single_threaded)110const Impl = if (builtin.single_threaded)
33 SingleThreadedCondition111 SingleThreadedImpl
34else if (builtin.os.tag == .windows)112else if (builtin.os.tag == .windows)
35 WindowsCondition113 WindowsImpl
36else if (std.Thread.use_pthreads)
37 PthreadCondition
38else114else
39 AtomicCondition;115 FutexImpl;
40116
41pub const SingleThreadedCondition = struct {117const Notify = enum {
42 pub fn wait(cond: *SingleThreadedCondition, mutex: *Mutex) void {118 one, // wake up only one thread
43 _ = cond;119 all, // wake up all threads
44 _ = mutex;120};
45 unreachable; // deadlock detected
46 }
47121
48 pub fn timedWait(cond: *SingleThreadedCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {122const SingleThreadedImpl = struct {
49 _ = cond;123 fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void {
124 _ = self;
50 _ = mutex;125 _ = mutex;
51 _ = timeout_ns;
52 std.time.sleep(timeout_ns);
53 return error.TimedOut;
54 }
55126
56 pub fn signal(cond: *SingleThreadedCondition) void {127 // There are no other threads to wake us up.
57 _ = cond;128 // So if we wait without a timeout we would never wake up.
129 const timeout_ns = timeout orelse {
130 unreachable; // deadlock detected
131 };
132
133 std.time.sleep(timeout_ns);
134 return error.Timeout;
58 }135 }
59136
60 pub fn broadcast(cond: *SingleThreadedCondition) void {137 fn wake(self: *Impl, comptime notify: Notify) void {
61 _ = cond;138 // There are no other threads to wake up.
139 _ = self;
140 _ = notify;
62 }141 }
63};142};
64143
65pub const WindowsCondition = struct {144const WindowsImpl = struct {
66 cond: windows.CONDITION_VARIABLE = windows.CONDITION_VARIABLE_INIT,145 condition: os.windows.CONDITION_VARIABLE = .{},
67146
68 pub fn wait(cond: *WindowsCondition, mutex: *Mutex) void {147 fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void {
69 const rc = windows.kernel32.SleepConditionVariableSRW(148 var timeout_overflowed = false;
70 &cond.cond,149 var timeout_ms: os.windows.DWORD = os.windows.INFINITE;
71 &mutex.impl.srwlock,
72 windows.INFINITE,
73 @as(windows.ULONG, 0),
74 );
75 assert(rc != windows.FALSE);
76 }
77150
78 pub fn timedWait(cond: *WindowsCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {151 if (timeout) |timeout_ns| {
79 var timeout_checked = std.math.cast(windows.DWORD, timeout_ns / std.time.ns_per_ms) catch overflow: {152 // Round the nanoseconds to the nearest millisecond,
80 break :overflow std.math.maxInt(windows.DWORD);153 // then saturating cast it to windows DWORD for use in kernel32 call.
81 };154 const ms = (timeout_ns +| (std.time.ns_per_ms / 2)) / std.time.ns_per_ms;
155 timeout_ms = std.math.cast(os.windows.DWORD, ms) catch std.math.maxInt(os.windows.DWORD);
82156
83 // Handle the case where timeout is INFINITE, otherwise SleepConditionVariableSRW's time-out never elapses157 // Track if the timeout overflowed into INFINITE and make sure not to wait forever.
84 const timeout_overflowed = timeout_checked == windows.INFINITE;158 if (timeout_ms == os.windows.INFINITE) {
85 timeout_checked -= @boolToInt(timeout_overflowed);159 timeout_overflowed = true;
160 timeout_ms -= 1;
161 }
162 }
86163
87 const rc = windows.kernel32.SleepConditionVariableSRW(164 const rc = os.windows.kernel32.SleepConditionVariableSRW(
88 &cond.cond,165 &self.condition,
89 &mutex.impl.srwlock,166 &mutex.impl.srwlock,
90 timeout_checked,167 timeout_ms,
91 @as(windows.ULONG, 0),168 0, // the srwlock was assumed to acquired in exclusive mode not shared
92 );169 );
93 if (rc == windows.FALSE and windows.kernel32.GetLastError() == windows.Win32Error.TIMEOUT) return error.TimedOut;
94 assert(rc != windows.FALSE);
95 }
96170
97 pub fn signal(cond: *WindowsCondition) void {171 // Return error.Timeout if we know the timeout elapsed correctly.
98 windows.kernel32.WakeConditionVariable(&cond.cond);172 if (rc == os.windows.FALSE) {
173 assert(os.windows.kernel32.GetLastError() == .TIMEOUT);
174 if (!timeout_overflowed) return error.Timeout;
175 }
99 }176 }
100177
101 pub fn broadcast(cond: *WindowsCondition) void {178 fn wake(self: *Impl, comptime notify: Notify) void {
102 windows.kernel32.WakeAllConditionVariable(&cond.cond);179 switch (notify) {
180 .one => os.windows.kernel32.WakeConditionVariable(&self.condition),
181 .all => os.windows.kernel32.WakeAllConditionVariable(&self.condition),
182 }
103 }183 }
104};184};
105185
106pub const PthreadCondition = struct {186const FutexImpl = struct {
107 cond: std.c.pthread_cond_t = .{},187 state: Atomic(u32) = Atomic(u32).init(0),
188 epoch: Atomic(u32) = Atomic(u32).init(0),
108189
109 pub fn wait(cond: *PthreadCondition, mutex: *Mutex) void {190 const one_waiter = 1;
110 const rc = std.c.pthread_cond_wait(&cond.cond, &mutex.impl.pthread_mutex);191 const waiter_mask = 0xffff;
111 assert(rc == .SUCCESS);
112 }
113192
114 pub fn timedWait(cond: *PthreadCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {193 const one_signal = 1 << 16;
115 var ts: std.os.timespec = undefined;194 const signal_mask = 0xffff << 16;
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 }
123195
124 const rc = std.c.pthread_cond_timedwait(&cond.cond, &mutex.impl.pthread_mutex, &ts);196 fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void {
125 return switch (rc) {197 // Register that we're waiting on the state by incrementing the wait count.
126 .SUCCESS => {},198 // This assumes that there can be at most ((1<<16)-1) or 65,355 threads concurrently waiting on the same Condvar.
127 .TIMEDOUT => error.TimedOut,199 // If this is hit in practice, then this condvar not working is the least of your concerns.
128 else => unreachable,200 var state = self.state.fetchAdd(one_waiter, .Monotonic);
129 };201 assert(state & waiter_mask != waiter_mask);
130 }202 state += one_waiter;
131203
132 pub fn signal(cond: *PthreadCondition) void {204 // Temporarily release the mutex in order to block on the condition variable.
133 const rc = std.c.pthread_cond_signal(&cond.cond);205 mutex.unlock();
134 assert(rc == .SUCCESS);206 defer mutex.lock();
135 }
136207
137 pub fn broadcast(cond: *PthreadCondition) void {208 var futex_deadline = Futex.Deadline.init(timeout);
138 const rc = std.c.pthread_cond_broadcast(&cond.cond);209 while (true) {
139 assert(rc == .SUCCESS);210 // Try to wake up by consuming a signal and decremented the waiter we added previously.
140 }211 // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return.
141};212 while (state & signal_mask != 0) {
213 const new_state = state - one_waiter - one_signal;
214 state = self.state.tryCompareAndSwap(state, new_state, .Acquire, .Monotonic) orelse return;
215 }
142216
143pub const AtomicCondition = struct {217 // Observe the epoch, then check the state again to see if we should wake up.
144 pending: bool = false,218 // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock:
145 queue_mutex: Mutex = .{},219 //
146 queue_list: QueueList = .{},220 // - T1: s = LOAD(&state)
147221 // - T2: UPDATE(&s, signal)
148 pub const QueueList = std.SinglyLinkedList(QueueItem);222 // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch)
149223 // - T1: e = LOAD(&epoch) (was reordered after the state load)
150 pub const QueueItem = struct {224 // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change)
151 futex: i32 = 0,225 //
152 dequeued: bool = false,226 // Acquire barrier to ensure the epoch load happens before the state load.
153227 const epoch = self.epoch.load(.Acquire);
154 fn wait(cond: *@This()) void {228 state = self.state.load(.Monotonic);
155 while (@atomicLoad(i32, &cond.futex, .Acquire) == 0) {229 if (state & signal_mask != 0) {
156 switch (builtin.os.tag) {230 continue;
157 .linux => {
158 switch (linux.getErrno(linux.futex_wait(
159 &cond.futex,
160 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT,
161 0,
162 null,
163 ))) {
164 .SUCCESS => {},
165 .INTR => {},
166 .AGAIN => {},
167 else => unreachable,
168 }
169 },
170 else => std.atomic.spinLoopHint(),
171 }
172 }231 }
173 }
174232
175 pub fn timedWait(cond: *@This(), timeout_ns: u64) error{TimedOut}!void {233 futex_deadline.wait(&self.epoch, epoch) catch |err| switch (err) {
176 const start_time = std.time.nanoTimestamp();234 // On timeout, we must decrement the waiter we added above.
177 while (@atomicLoad(i32, &cond.futex, .Acquire) == 0) {235 error.Timeout => {
178 switch (builtin.os.tag) {236 while (true) {
179 .linux => {237 // If there's a signal when we're timing out, consume it and report being woken up instead.
180 var ts: std.os.timespec = undefined;238 // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return.
181 ts.tv_sec = @intCast(@TypeOf(ts.tv_sec), timeout_ns / std.time.ns_per_s);239 while (state & signal_mask != 0) {
182 ts.tv_nsec = @intCast(@TypeOf(ts.tv_nsec), timeout_ns % std.time.ns_per_s);240 const new_state = state - one_waiter - one_signal;
183 switch (linux.getErrno(linux.futex_wait(241 state = self.state.tryCompareAndSwap(state, new_state, .Acquire, .Monotonic) orelse return;
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 }242 }
202 std.atomic.spinLoopHint();
203 },
204 }
205 }
206 }
207243
208 fn notify(cond: *@This()) void {244 // Remove the waiter we added and officially return timed out.
209 @atomicStore(i32, &cond.futex, 1, .Release);245 const new_state = state - one_waiter;
210246 state = self.state.tryCompareAndSwap(state, new_state, .Monotonic, .Monotonic) orelse return err;
211 switch (builtin.os.tag) {
212 .linux => {
213 switch (linux.getErrno(linux.futex_wake(
214 &cond.futex,
215 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE,
216 1,
217 ))) {
218 .SUCCESS => {},
219 .FAULT => {},
220 else => unreachable,
221 }247 }
222 },248 },
223 else => {},249 };
250 }
251 }
252
253 fn wake(self: *Impl, comptime notify: Notify) void {
254 var state = self.state.load(.Monotonic);
255 while (true) {
256 const waiters = (state & waiter_mask) / one_waiter;
257 const signals = (state & signal_mask) / one_signal;
258
259 // Reserves which waiters to wake up by incrementing the signals count.
260 // Therefor, the signals count is always less than or equal to the waiters count.
261 // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters.
262 const wakeable = waiters - signals;
263 if (wakeable == 0) {
264 return;
224 }265 }
266
267 const to_wake = switch (notify) {
268 .one => 1,
269 .all => wakeable,
270 };
271
272 // Reserve the amount of waiters to wake by incrementing the signals count.
273 // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads.
274 const new_state = state + (one_signal * to_wake);
275 state = self.state.tryCompareAndSwap(state, new_state, .Release, .Monotonic) orelse {
276 // Wake up the waiting threads we reserved above by changing the epoch value.
277 // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it.
278 // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption.
279 //
280 // Release barrier ensures the signal being added to the state happens before the epoch is changed.
281 // If not, the waiting thread could potentially deadlock from missing both the state and epoch change:
282 //
283 // - T2: UPDATE(&epoch, 1) (reordered before the state change)
284 // - T1: e = LOAD(&epoch)
285 // - T1: s = LOAD(&state)
286 // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch)
287 // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change)
288 _ = self.epoch.fetchAdd(1, .Release);
289 Futex.wake(&self.epoch, to_wake);
290 return;
291 };
225 }292 }
226 };293 }
294};
227295
228 pub fn wait(cond: *AtomicCondition, mutex: *Mutex) void {296test "Condition - smoke test" {
229 var waiter = QueueList.Node{ .data = .{} };297 var mutex = Mutex{};
298 var cond = Condition{};
230299
231 {300 // Try to wake outside the mutex
232 cond.queue_mutex.lock();301 defer cond.signal();
233 defer cond.queue_mutex.unlock();302 defer cond.broadcast();
234303
235 cond.queue_list.prepend(&waiter);304 mutex.lock();
236 @atomicStore(bool, &cond.pending, true, .SeqCst);305 defer mutex.unlock();
237 }
238306
239 mutex.unlock();307 // Try to wait with a timeout (should not deadlock)
240 waiter.data.wait();308 try testing.expectError(error.Timeout, cond.timedWait(&mutex, 0));
241 mutex.lock();309 try testing.expectError(error.Timeout, cond.timedWait(&mutex, std.time.ns_per_ms));
242 }
243310
244 pub fn timedWait(cond: *AtomicCondition, mutex: *Mutex, timeout_ns: u64) error{TimedOut}!void {311 // Try to wake inside the mutex.
245 var waiter = QueueList.Node{ .data = .{} };312 cond.signal();
313 cond.broadcast();
314}
246315
247 {316// Inspired from: https://github.com/Amanieu/parking_lot/pull/129
248 cond.queue_mutex.lock();317test "Condition - wait and signal" {
249 defer cond.queue_mutex.unlock();318 // This test requires spawning threads
319 if (builtin.single_threaded) {
320 return error.SkipZigTest;
321 }
250322
251 cond.queue_list.prepend(&waiter);323 const num_threads = 4;
252 @atomicStore(bool, &cond.pending, true, .SeqCst);
253 }
254324
255 var timed_out = false;325 const MultiWait = struct {
256 mutex.unlock();326 mutex: Mutex = .{},
257 defer mutex.lock();327 cond: Condition = .{},
258 waiter.data.timedWait(timeout_ns) catch |err| switch (err) {328 threads: [num_threads]std.Thread = undefined,
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 };
273329
274 if (timed_out) {330 fn run(self: *@This()) void {
275 return error.TimedOut;331 self.mutex.lock();
332 defer self.mutex.unlock();
333
334 self.cond.wait(&self.mutex);
335 self.cond.timedWait(&self.mutex, std.time.ns_per_ms) catch {};
336 self.cond.signal();
276 }337 }
338 };
339
340 var multi_wait = MultiWait{};
341 for (multi_wait.threads) |*t| {
342 t.* = try std.Thread.spawn(.{}, MultiWait.run, .{&multi_wait});
277 }343 }
278344
279 pub fn signal(cond: *AtomicCondition) void {345 std.time.sleep(100 * std.time.ns_per_ms);
280 if (@atomicLoad(bool, &cond.pending, .SeqCst) == false)
281 return;
282346
283 const maybe_waiter = blk: {347 multi_wait.cond.signal();
284 cond.queue_mutex.lock();348 for (multi_wait.threads) |t| {
285 defer cond.queue_mutex.unlock();349 t.join();
350 }
351}
286352
287 const maybe_waiter = cond.queue_list.popFirst();353test "Condition - signal" {
288 if (maybe_waiter) |waiter| {354 // This test requires spawning threads
289 waiter.data.dequeued = true;355 if (builtin.single_threaded) {
356 return error.SkipZigTest;
357 }
358
359 const num_threads = 4;
360
361 const SignalTest = struct {
362 mutex: Mutex = .{},
363 cond: Condition = .{},
364 notified: bool = false,
365 threads: [num_threads]std.Thread = undefined,
366
367 fn run(self: *@This()) void {
368 self.mutex.lock();
369 defer self.mutex.unlock();
370
371 // Use timedWait() a few times before using wait()
372 // to test multiple threads timing out frequently.
373 var i: usize = 0;
374 while (!self.notified) : (i +%= 1) {
375 if (i < 5) {
376 self.cond.timedWait(&self.mutex, 1) catch {};
377 } else {
378 self.cond.wait(&self.mutex);
379 }
290 }380 }
291 @atomicStore(bool, &cond.pending, cond.queue_list.first != null, .SeqCst);
292 break :blk maybe_waiter;
293 };
294381
295 if (maybe_waiter) |waiter| {382 // Once we received the signal, notify another thread (inside the lock).
296 waiter.data.notify();383 assert(self.notified);
384 self.cond.signal();
297 }385 }
298 }386 };
299
300 pub fn broadcast(cond: *AtomicCondition) void {
301 if (@atomicLoad(bool, &cond.pending, .SeqCst) == false)
302 return;
303387
304 @atomicStore(bool, &cond.pending, false, .SeqCst);388 var signal_test = SignalTest{};
389 for (signal_test.threads) |*t| {
390 t.* = try std.Thread.spawn(.{}, SignalTest.run, .{&signal_test});
391 }
305392
306 var waiters = blk: {393 {
307 cond.queue_mutex.lock();394 // Wait for a bit in hopes that the spawned threads start queuing up on the condvar
308 defer cond.queue_mutex.unlock();395 std.time.sleep(10 * std.time.ns_per_ms);
309396
310 const waiters = cond.queue_list;397 // Wake up one of them (outside the lock) after setting notified=true.
398 defer signal_test.cond.signal();
311399
312 var it = waiters.first;400 signal_test.mutex.lock();
313 while (it) |node| : (it = node.next) {401 defer signal_test.mutex.unlock();
314 node.data.dequeued = true;
315 }
316402
317 cond.queue_list = .{};403 try testing.expect(!signal_test.notified);
318 break :blk waiters;404 signal_test.notified = true;
319 };405 }
320406
321 while (waiters.popFirst()) |waiter| {407 for (signal_test.threads) |t| {
322 waiter.data.notify();408 t.join();
323 }
324 }409 }
325};410}
326411
327test "Thread.Condition" {412test "Condition - multi signal" {
413 // This test requires spawning threads
328 if (builtin.single_threaded) {414 if (builtin.single_threaded) {
329 return error.SkipZigTest;415 return error.SkipZigTest;
330 }416 }
331417
332 const TestContext = struct {418 const num_threads = 4;
333 cond: *Condition,419 const num_iterations = 4;
334 cond_main: *Condition,420
335 mutex: *Mutex,421 const Paddle = struct {
336 n: *i32,422 mutex: Mutex = .{},
337 fn worker(ctx: *@This()) void {423 cond: Condition = .{},
338 ctx.mutex.lock();424 value: u32 = 0,
339 ctx.n.* += 1;425
340 ctx.cond_main.signal();426 fn hit(self: *@This()) void {
341 ctx.cond.wait(ctx.mutex);427 defer self.cond.signal();
342 ctx.n.* -= 1;428
343 ctx.cond_main.signal();429 self.mutex.lock();
344 ctx.mutex.unlock();430 defer self.mutex.unlock();
431
432 self.value += 1;
345 }433 }
346 };
347 const num_threads = 3;
348 var threads: [num_threads]std.Thread = undefined;
349 var cond = Condition{};
350 var cond_main = Condition{};
351 var mut = Mutex{};
352 var n: i32 = 0;
353 var ctx = TestContext{ .cond = &cond, .cond_main = &cond_main, .mutex = &mut, .n = &n };
354434
355 mut.lock();435 fn run(self: *@This(), hit_to: *@This()) !void {
356 for (threads) |*t| t.* = try std.Thread.spawn(.{}, TestContext.worker, .{&ctx});436 self.mutex.lock();
357 cond_main.wait(&mut);437 defer self.mutex.unlock();
358 while (n < num_threads) cond_main.wait(&mut);
359438
360 cond.signal();439 var current: u32 = 0;
361 cond_main.wait(&mut);440 while (current < num_iterations) : (current += 1) {
362 try testing.expect(n == (num_threads - 1));441 // Wait for the value to change from hit()
442 while (self.value == current) {
443 self.cond.wait(&self.mutex);
444 }
363445
364 cond.broadcast();446 // hit the next paddle
365 while (n > 0) cond_main.wait(&mut);447 try testing.expectEqual(self.value, current + 1);
366 try testing.expect(n == 0);448 hit_to.hit();
449 }
450 }
451 };
452
453 var paddles = [_]Paddle{.{}} ** num_threads;
454 var threads = [_]std.Thread{undefined} ** num_threads;
455
456 // Create a circle of paddles which hit each other
457 for (threads) |*t, i| {
458 const paddle = &paddles[i];
459 const hit_to = &paddles[(i + 1) % paddles.len];
460 t.* = try std.Thread.spawn(.{}, Paddle.run, .{ paddle, hit_to });
461 }
367462
463 // Hit the first paddle and wait for them all to complete by hitting each other for num_iterations.
464 paddles[0].hit();
368 for (threads) |t| t.join();465 for (threads) |t| t.join();
466
467 // The first paddle will be hit one last time by the last paddle.
468 for (paddles) |p, i| {
469 const expected = @as(u32, num_iterations) + @boolToInt(i == 0);
470 try testing.expectEqual(p.value, expected);
471 }
369}472}
370473
371test "Thread.Condition.timedWait" {474test "Condition - broadcasting" {
475 // This test requires spawning threads
372 if (builtin.single_threaded) {476 if (builtin.single_threaded) {
373 return error.SkipZigTest;477 return error.SkipZigTest;
374 }478 }
375479
376 var cond = Condition{};480 const num_threads = 10;
377 var mut = Mutex{};
378481
379 // Expect a timeout, as the condition variable is never signaled482 const BroadcastTest = struct {
380 {483 mutex: Mutex = .{},
381 mut.lock();484 cond: Condition = .{},
382 defer mut.unlock();485 completed: Condition = .{},
383 try testing.expectError(error.TimedOut, cond.timedWait(&mut, 10 * std.time.ns_per_ms));486 count: usize = 0,
487 threads: [num_threads]std.Thread = undefined,
488
489 fn run(self: *@This()) void {
490 self.mutex.lock();
491 defer self.mutex.unlock();
492
493 // The last broadcast thread to start tells the main test thread it's completed.
494 self.count += 1;
495 if (self.count == num_threads) {
496 self.completed.signal();
497 }
498
499 // Waits for the count to reach zero after the main test thread observes it at num_threads.
500 // Tries to use timedWait() a bit before falling back to wait() to test multiple threads timing out.
501 var i: usize = 0;
502 while (self.count != 0) : (i +%= 1) {
503 if (i < 10) {
504 self.cond.timedWait(&self.mutex, 1) catch {};
505 } else {
506 self.cond.wait(&self.mutex);
507 }
508 }
509 }
510 };
511
512 var broadcast_test = BroadcastTest{};
513 for (broadcast_test.threads) |*t| {
514 t.* = try std.Thread.spawn(.{}, BroadcastTest.run, .{&broadcast_test});
384 }515 }
385516
386 // Expect a signal before timeout
387 {517 {
388 const TestContext = struct {518 broadcast_test.mutex.lock();
389 cond: *Condition,519 defer broadcast_test.mutex.unlock();
390 mutex: *Mutex,520
391 n: *u32,521 // Wait for all the broadcast threads to spawn.
392 fn worker(ctx: *@This()) void {522 // timedWait() to detect any potential deadlocks.
393 ctx.mutex.lock();523 while (broadcast_test.count != num_threads) {
394 defer ctx.mutex.unlock();524 try broadcast_test.completed.timedWait(
395 ctx.n.* = 1;525 &broadcast_test.mutex,
396 ctx.cond.signal();526 1 * std.time.ns_per_s,
397 }527 );
398 };528 }
399529
400 var n: u32 = 0;530 // Reset the counter and wake all the threads to exit.
531 broadcast_test.count = 0;
532 broadcast_test.cond.broadcast();
533 }
401534
402 var ctx = TestContext{ .cond = &cond, .mutex = &mut, .n = &n };535 for (broadcast_test.threads) |t| {
403 mut.lock();536 t.join();
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 }537 }
411}538}
lib/std/Thread/Futex.zig+62-7
...@@ -10,14 +10,12 @@ const Futex = @This();...@@ -10,14 +10,12 @@ const Futex = @This();
10const os = std.os;10const os = std.os;
11const assert = std.debug.assert;11const assert = std.debug.assert;
12const testing = std.testing;12const testing = std.testing;
13
14const Atomic = std.atomic.Atomic;13const Atomic = std.atomic.Atomic;
15const spinLoopHint = std.atomic.spinLoopHint;
1614
17/// Checks if `ptr` still contains the value `expect` and, if so, blocks the caller until either:15/// Checks if `ptr` still contains the value `expect` and, if so, blocks the caller until either:
18/// - The value at `ptr` is no longer equal to `expect`.16/// - The value at `ptr` is no longer equal to `expect`.
19/// - The caller is unblocked by a matching `wake()`.17/// - The caller is unblocked by a matching `wake()`.
20/// - The caller is unblocked spuriously by an arbitrary internal signal.18/// - The caller is unblocked spuriously ("at random").
21///19///
22/// The checking of `ptr` and `expect`, along with blocking the caller, is done atomically20/// The checking of `ptr` and `expect`, along with blocking the caller, is done atomically
23/// and totally ordered (sequentially consistent) with respect to other wait()/wake() calls on the same `ptr`.21/// and totally ordered (sequentially consistent) with respect to other wait()/wake() calls on the same `ptr`.
...@@ -32,7 +30,7 @@ pub fn wait(ptr: *const Atomic(u32), expect: u32) void {...@@ -32,7 +30,7 @@ pub fn wait(ptr: *const Atomic(u32), expect: u32) void {
32/// Checks if `ptr` still contains the value `expect` and, if so, blocks the caller until either:30/// Checks if `ptr` still contains the value `expect` and, if so, blocks the caller until either:
33/// - The value at `ptr` is no longer equal to `expect`.31/// - The value at `ptr` is no longer equal to `expect`.
34/// - The caller is unblocked by a matching `wake()`.32/// - The caller is unblocked by a matching `wake()`.
35/// - The caller is unblocked spuriously by an arbitrary internal signal.33/// - The caller is unblocked spuriously ("at random").
36/// - The caller blocks for longer than the given timeout. In which case, `error.Timeout` is returned.34/// - The caller blocks for longer than the given timeout. In which case, `error.Timeout` is returned.
37///35///
38/// The checking of `ptr` and `expect`, along with blocking the caller, is done atomically36/// The checking of `ptr` and `expect`, along with blocking the caller, is done atomically
...@@ -62,7 +60,7 @@ pub fn wake(ptr: *const Atomic(u32), max_waiters: u32) void {...@@ -62,7 +60,7 @@ pub fn wake(ptr: *const Atomic(u32), max_waiters: u32) void {
62}60}
6361
64const Impl = if (builtin.single_threaded)62const Impl = if (builtin.single_threaded)
65 SerialImpl63 SingleThreadedImpl
66else if (builtin.os.tag == .windows)64else if (builtin.os.tag == .windows)
67 WindowsImpl65 WindowsImpl
68else if (builtin.os.tag.isDarwin())66else if (builtin.os.tag.isDarwin())
...@@ -97,7 +95,7 @@ const UnsupportedImpl = struct {...@@ -97,7 +95,7 @@ const UnsupportedImpl = struct {
97 }95 }
98};96};
9997
100const SerialImpl = struct {98const SingleThreadedImpl = struct {
101 fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{Timeout}!void {99 fn wait(ptr: *const Atomic(u32), expect: u32, timeout: ?u64) error{Timeout}!void {
102 if (ptr.loadUnchecked() != expect) {100 if (ptr.loadUnchecked() != expect) {
103 return;101 return;
...@@ -804,7 +802,7 @@ const PosixImpl = struct {...@@ -804,7 +802,7 @@ const PosixImpl = struct {
804 //802 //
805 // What we really want here is a Release load, but that doesn't exist under the C11 memory model.803 // What we really want here is a Release load, but that doesn't exist under the C11 memory model.
806 // We could instead do `bucket.pending.fetchAdd(0, Release) == 0` which achieves effectively the same thing,804 // We could instead do `bucket.pending.fetchAdd(0, Release) == 0` which achieves effectively the same thing,
807 // but the RMW operation unconditionally stores which invalidates the cache-line for others causing unnecessary contention.805 // but the RMW operation unconditionally marks the cache-line as modified for others causing unnecessary fetching/contention.
808 //806 //
809 // Instead we opt to do a full-fence + load instead which avoids taking ownership of the cache-line.807 // Instead we opt to do a full-fence + load instead which avoids taking ownership of the cache-line.
810 // fence(SeqCst) effectively converts the ptr update to SeqCst and the pending load to SeqCst: creating a Store-Load barrier.808 // fence(SeqCst) effectively converts the ptr update to SeqCst and the pending load to SeqCst: creating a Store-Load barrier.
...@@ -962,3 +960,60 @@ test "Futex - broadcasting" {...@@ -962,3 +960,60 @@ test "Futex - broadcasting" {
962 for (broadcast.threads) |*t| t.* = try std.Thread.spawn(.{}, Broadcast.run, .{&broadcast});960 for (broadcast.threads) |*t| t.* = try std.Thread.spawn(.{}, Broadcast.run, .{&broadcast});
963 for (broadcast.threads) |t| t.join();961 for (broadcast.threads) |t| t.join();
964}962}
963
964/// Deadline is used to wait efficiently for a pointer's value to change using Futex and a fixed timeout.
965///
966/// Futex's timedWait() api uses a relative duration which suffers from over-waiting
967/// when used in a loop which is often required due to the possibility of spurious wakeups.
968///
969/// Deadline instead converts the relative timeout to an absolute one so that multiple calls
970/// to Futex timedWait() can block for and report more accurate error.Timeouts.
971pub const Deadline = struct {
972 timeout: ?u64,
973 started: std.time.Timer,
974
975 /// Create the deadline to expire after the given amount of time in nanoseconds passes.
976 /// Pass in `null` to have the deadline call `Futex.wait()` and never expire.
977 pub fn init(expires_in_ns: ?u64) Deadline {
978 var deadline: Deadline = undefined;
979 deadline.timeout = expires_in_ns;
980
981 // std.time.Timer is required to be supported for somewhat accurate reportings of error.Timeout.
982 if (deadline.timeout != null) {
983 deadline.started = std.time.Timer.start() catch unreachable;
984 }
985
986 return deadline;
987 }
988
989 /// Wait until either:
990 /// - the `ptr`'s value changes from `expect`.
991 /// - `Futex.wake()` is called on the `ptr`.
992 /// - A spurious wake occurs.
993 /// - The deadline expires; In which case `error.Timeout` is returned.
994 pub fn wait(self: *Deadline, ptr: *const Atomic(u32), expect: u32) error{Timeout}!void {
995 @setCold(true);
996
997 // Check if we actually have a timeout to wait until.
998 // If not just wait "forever".
999 const timeout_ns = self.timeout orelse {
1000 return Futex.wait(ptr, expect);
1001 };
1002
1003 // Get how much time has passed since we started waiting
1004 // then subtract that from the init() timeout to get how much longer to wait.
1005 // Use overflow to detect when we've been waiting longer than the init() timeout.
1006 const elapsed_ns = self.started.read();
1007 const until_timeout_ns = std.math.sub(u64, timeout_ns, elapsed_ns) catch 0;
1008 return Futex.timedWait(ptr, expect, until_timeout_ns);
1009 }
1010};
1011
1012test "Futex - Deadline" {
1013 var deadline = Deadline.init(100 * std.time.ns_per_ms);
1014 var futex_word = Atomic(u32).init(0);
1015
1016 while (true) {
1017 deadline.wait(&futex_word, 0) catch break;
1018 }
1019}
lib/std/Thread/Mutex.zig+208-211
...@@ -1,288 +1,285 @@...@@ -1,288 +1,285 @@
1//! Lock may be held only once. If the same thread tries to acquire1//! Mutex is a synchronization primitive which enforces atomic access to a shared region of code known as the "critical section".
2//! the same mutex twice, it deadlocks. This type supports static2//! It does this by blocking ensuring only one thread is in the critical section at any given point in time by blocking the others.
3//! initialization and is at most `@sizeOf(usize)` in size. When an3//! Mutex can be statically initialized and is at most `@sizeOf(u64)` large.
4//! application is built in single threaded release mode, all the4//! Use `lock()` or `tryLock()` to enter the critical section and `unlock()` to leave it.
5//! functions are no-ops. In single threaded debug mode, there is
6//! deadlock detection.
7//!5//!
8//! Example usage:6//! Example:
7//! ```
9//! var m = Mutex{};8//! var m = Mutex{};
10//!9//!
11//! m.lock();10//! {
12//! defer m.release();11//! m.lock();
13//! ... critical code12//! defer m.unlock();
13//! // ... critical section code
14//! }
14//!15//!
15//! Non-blocking:
16//! if (m.tryLock()) {16//! if (m.tryLock()) {
17//! defer m.unlock();17//! defer m.unlock();
18//! // ... critical section18//! // ... critical section code
19//! } else {
20//! // ... lock not acquired
21//! }19//! }
20//! ```
2221
23impl: Impl = .{},
24
25const Mutex = @This();
26const std = @import("../std.zig");22const std = @import("../std.zig");
27const builtin = @import("builtin");23const builtin = @import("builtin");
24const Mutex = @This();
25
28const os = std.os;26const os = std.os;
29const assert = std.debug.assert;27const assert = std.debug.assert;
30const windows = os.windows;
31const linux = os.linux;
32const testing = std.testing;28const testing = std.testing;
33const StaticResetEvent = std.thread.StaticResetEvent;29const Atomic = std.atomic.Atomic;
30const Futex = std.Thread.Futex;
31
32impl: Impl = .{},
3433
35/// Try to acquire the mutex without blocking. Returns `false` if the mutex is34/// Tries to acquire the mutex without blocking the caller's thread.
36/// unavailable. Otherwise returns `true`. Call `unlock` on the mutex to release.35/// Returns `false` if the calling thread would have to block to acquire it.
37pub fn tryLock(m: *Mutex) bool {36/// Otherwise, returns `true` and the caller should `unlock()` the Mutex to release it.
38 return m.impl.tryLock();37pub fn tryLock(self: *Mutex) bool {
38 return self.impl.tryLock();
39}39}
4040
41/// Acquire the mutex. Deadlocks if the mutex is already41/// Acquires the mutex, blocking the caller's thread until it can.
42/// held by the calling thread.42/// It is undefined behavior if the mutex is already held by the caller's thread.
43pub fn lock(m: *Mutex) void {43/// Once acquired, call `unlock()` on the Mutex to release it.
44 m.impl.lock();44pub fn lock(self: *Mutex) void {
45 self.impl.lock();
45}46}
4647
47pub fn unlock(m: *Mutex) void {48/// Releases the mutex which was previously acquired with `lock()` or `tryLock()`.
48 m.impl.unlock();49/// It is undefined behavior if the mutex is unlocked from a different thread that it was locked from.
50pub fn unlock(self: *Mutex) void {
51 self.impl.unlock();
49}52}
5053
51const Impl = if (builtin.single_threaded)54const Impl = if (builtin.single_threaded)
52 Dummy55 SingleThreadedImpl
53else if (builtin.os.tag == .windows)56else if (builtin.os.tag == .windows)
54 WindowsMutex57 WindowsImpl
55else if (std.Thread.use_pthreads)58else if (builtin.os.tag.isDarwin())
56 PthreadMutex59 DarwinImpl
57else60else
58 AtomicMutex;61 FutexImpl;
59
60pub const AtomicMutex = struct {
61 state: State = .unlocked,
6262
63 const State = enum(i32) {63const SingleThreadedImpl = struct {
64 unlocked,64 is_locked: bool = false,
65 locked,
66 waiting,
67 };
6865
69 pub fn tryLock(m: *AtomicMutex) bool {66 fn tryLock(self: *Impl) bool {
70 return @cmpxchgStrong(67 if (self.is_locked) return false;
71 State,68 self.is_locked = true;
72 &m.state,69 return true;
73 .unlocked,
74 .locked,
75 .Acquire,
76 .Monotonic,
77 ) == null;
78 }70 }
7971
80 pub fn lock(m: *AtomicMutex) void {72 fn lock(self: *Impl) void {
81 switch (@atomicRmw(State, &m.state, .Xchg, .locked, .Acquire)) {73 if (!self.tryLock()) {
82 .unlocked => {},74 unreachable; // deadlock detected
83 else => |s| m.lockSlow(s),
84 }75 }
85 }76 }
8677
87 pub fn unlock(m: *AtomicMutex) void {78 fn unlock(self: *Impl) void {
88 switch (@atomicRmw(State, &m.state, .Xchg, .unlocked, .Release)) {79 assert(self.is_locked);
89 .unlocked => unreachable,80 self.is_locked = false;
90 .locked => {},
91 .waiting => m.unlockSlow(),
92 }
93 }81 }
82};
9483
95 fn lockSlow(m: *AtomicMutex, current_state: State) void {84// SRWLOCK on windows is almost always faster than Futex solution.
96 @setCold(true);85// It also implements an efficient Condition with requeue support for us.
97 var new_state = current_state;86const WindowsImpl = struct {
9887 srwlock: os.windows.SRWLOCK = .{},
99 var spin: u8 = 0;
100 while (spin < 100) : (spin += 1) {
101 const state = @cmpxchgWeak(
102 State,
103 &m.state,
104 .unlocked,
105 new_state,
106 .Acquire,
107 .Monotonic,
108 ) orelse return;
109
110 switch (state) {
111 .unlocked => {},
112 .locked => {},
113 .waiting => break,
114 }
115
116 var iter = std.math.min(32, spin + 1);
117 while (iter > 0) : (iter -= 1)
118 std.atomic.spinLoopHint();
119 }
12088
121 new_state = .waiting;89 fn tryLock(self: *Impl) bool {
122 while (true) {90 return os.windows.kernel32.TryAcquireSRWLockExclusive(&self.srwlock) != os.windows.FALSE;
123 switch (@atomicRmw(State, &m.state, .Xchg, new_state, .Acquire)) {
124 .unlocked => return,
125 else => {},
126 }
127 switch (builtin.os.tag) {
128 .linux => {
129 switch (linux.getErrno(linux.futex_wait(
130 @ptrCast(*const i32, &m.state),
131 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAIT,
132 @enumToInt(new_state),
133 null,
134 ))) {
135 .SUCCESS => {},
136 .INTR => {},
137 .AGAIN => {},
138 else => unreachable,
139 }
140 },
141 else => std.atomic.spinLoopHint(),
142 }
143 }
144 }91 }
14592
146 fn unlockSlow(m: *AtomicMutex) void {93 fn lock(self: *Impl) void {
147 @setCold(true);94 os.windows.kernel32.AcquireSRWLockExclusive(&self.srwlock);
95 }
14896
149 switch (builtin.os.tag) {97 fn unlock(self: *Impl) void {
150 .linux => {98 os.windows.kernel32.ReleaseSRWLockExclusive(&self.srwlock);
151 switch (linux.getErrno(linux.futex_wake(
152 @ptrCast(*const i32, &m.state),
153 linux.FUTEX.PRIVATE_FLAG | linux.FUTEX.WAKE,
154 1,
155 ))) {
156 .SUCCESS => {},
157 .FAULT => unreachable, // invalid pointer passed to futex_wake
158 else => unreachable,
159 }
160 },
161 else => {},
162 }
163 }99 }
164};100};
165101
166pub const PthreadMutex = struct {102// os_unfair_lock on darwin supports priority inheritance and is generally faster than Futex solutions.
167 pthread_mutex: std.c.pthread_mutex_t = .{},103const DarwinImpl = struct {
104 oul: os.darwin.os_unfair_lock = .{},
168105
169 /// Try to acquire the mutex without blocking. Returns true if106 fn tryLock(self: *Impl) bool {
170 /// the mutex is unavailable. Otherwise returns false. Call107 return os.darwin.os_unfair_lock_trylock(&self.oul);
171 /// release when done.
172 pub fn tryLock(m: *PthreadMutex) bool {
173 return std.c.pthread_mutex_trylock(&m.pthread_mutex) == .SUCCESS;
174 }108 }
175109
176 /// Acquire the mutex. Will deadlock if the mutex is already110 fn lock(self: *Impl) void {
177 /// held by the calling thread.111 os.darwin.os_unfair_lock_lock(&self.oul);
178 pub fn lock(m: *PthreadMutex) void {
179 switch (std.c.pthread_mutex_lock(&m.pthread_mutex)) {
180 .SUCCESS => {},
181 .INVAL => unreachable,
182 .BUSY => unreachable,
183 .AGAIN => unreachable,
184 .DEADLK => unreachable,
185 .PERM => unreachable,
186 else => unreachable,
187 }
188 }112 }
189113
190 pub fn unlock(m: *PthreadMutex) void {114 fn unlock(self: *Impl) void {
191 switch (std.c.pthread_mutex_unlock(&m.pthread_mutex)) {115 os.darwin.os_unfair_lock_unlock(&self.oul);
192 .SUCCESS => return,
193 .INVAL => unreachable,
194 .AGAIN => unreachable,
195 .PERM => unreachable,
196 else => unreachable,
197 }
198 }116 }
199};117};
200118
201/// This has the sematics as `Mutex`, however it does not actually do any119const FutexImpl = struct {
202/// synchronization. Operations are safety-checked no-ops.120 state: Atomic(u32) = Atomic(u32).init(unlocked),
203pub const Dummy = struct {
204 locked: @TypeOf(lock_init) = lock_init,
205121
206 const lock_init = if (std.debug.runtime_safety) false else {};122 const unlocked = 0b00;
123 const locked = 0b01;
124 const contended = 0b11; // must contain the `locked` bit for x86 optimization below
125
126 fn tryLock(self: *Impl) bool {
127 // Lock with compareAndSwap instead of tryCompareAndSwap to avoid reporting spurious CAS failure.
128 return self.lockFast("compareAndSwap");
129 }
207130
208 /// Try to acquire the mutex without blocking. Returns false if131 fn lock(self: *Impl) void {
209 /// the mutex is unavailable. Otherwise returns true.132 // Lock with tryCompareAndSwap instead of compareAndSwap due to being more inline-able on LL/SC archs like ARM.
210 pub fn tryLock(m: *Dummy) bool {133 if (!self.lockFast("tryCompareAndSwap")) {
211 if (std.debug.runtime_safety) {134 self.lockSlow();
212 if (m.locked) return false;
213 m.locked = true;
214 }135 }
215 return true;
216 }136 }
217137
218 /// Acquire the mutex. Will deadlock if the mutex is already138 inline fn lockFast(self: *Impl, comptime casFn: []const u8) bool {
219 /// held by the calling thread.139 // On x86, use `lock bts` instead of `lock cmpxchg` as:
220 pub fn lock(m: *Dummy) void {140 // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048
221 if (!m.tryLock()) {141 // - `lock bts` is smaller instruction-wise which makes it better for inlining
222 @panic("deadlock detected");142 if (comptime builtin.target.cpu.arch.isX86()) {
143 const locked_bit = @ctz(u32, @as(u32, locked));
144 return self.state.bitSet(locked_bit, .Acquire) == 0;
223 }145 }
146
147 // Acquire barrier ensures grabbing the lock happens before the critical section
148 // and that the previous lock holder's critical section happens before we grab the lock.
149 return @field(self.state, casFn)(unlocked, locked, .Acquire, .Monotonic) == null;
224 }150 }
225151
226 pub fn unlock(m: *Dummy) void {152 fn lockSlow(self: *Impl) void {
227 if (std.debug.runtime_safety) {153 @setCold(true);
228 m.locked = false;154
155 // Avoid doing an atomic swap below if we already know the state is contended.
156 // An atomic swap unconditionally stores which marks the cache-line as modified unnecessarily.
157 if (self.state.load(.Monotonic) == contended) {
158 Futex.wait(&self.state, contended);
159 }
160
161 // Try to acquire the lock while also telling the existing lock holder that there are threads waiting.
162 //
163 // Once we sleep on the Futex, we must acquire the mutex using `contended` rather than `locked`.
164 // If not, threads sleeping on the Futex wouldn't see the state change in unlock and potentially deadlock.
165 // The downside is that the last mutex unlocker will see `contended` and do an unnecessary Futex wake
166 // but this is better than having to wake all waiting threads on mutex unlock.
167 //
168 // Acquire barrier ensures grabbing the lock happens before the critical section
169 // and that the previous lock holder's critical section happens before we grab the lock.
170 while (self.state.swap(contended, .Acquire) != unlocked) {
171 Futex.wait(&self.state, contended);
172 }
173 }
174
175 fn unlock(self: *Impl) void {
176 // Unlock the mutex and wake up a waiting thread if any.
177 //
178 // A waiting thread will acquire with `contended` instead of `locked`
179 // which ensures that it wakes up another thread on the next unlock().
180 //
181 // Release barrier ensures the critical section happens before we let go of the lock
182 // and that our critical section happens before the next lock holder grabs the lock.
183 const state = self.state.swap(unlocked, .Release);
184 assert(state != unlocked);
185
186 if (state == contended) {
187 Futex.wake(&self.state, 1);
229 }188 }
230 }189 }
231};190};
232191
233pub const WindowsMutex = struct {192test "Mutex - smoke test" {
234 srwlock: windows.SRWLOCK = windows.SRWLOCK_INIT,193 var mutex = Mutex{};
194
195 try testing.expect(mutex.tryLock());
196 try testing.expect(!mutex.tryLock());
197 mutex.unlock();
235198
236 pub fn tryLock(m: *WindowsMutex) bool {199 mutex.lock();
237 return windows.kernel32.TryAcquireSRWLockExclusive(&m.srwlock) != windows.FALSE;200 try testing.expect(!mutex.tryLock());
238 }201 mutex.unlock();
202}
203
204// A counter which is incremented without atomic instructions
205const NonAtomicCounter = struct {
206 // direct u128 could maybe use xmm ops on x86 which are atomic
207 value: [2]u64 = [_]u64{ 0, 0 },
239208
240 pub fn lock(m: *WindowsMutex) void {209 fn get(self: NonAtomicCounter) u128 {
241 windows.kernel32.AcquireSRWLockExclusive(&m.srwlock);210 return @bitCast(u128, self.value);
242 }211 }
243212
244 pub fn unlock(m: *WindowsMutex) void {213 fn inc(self: *NonAtomicCounter) void {
245 windows.kernel32.ReleaseSRWLockExclusive(&m.srwlock);214 for (@bitCast([2]u64, self.get() + 1)) |v, i| {
215 @ptrCast(*volatile u64, &self.value[i]).* = v;
216 }
246 }217 }
247};218};
248219
249const TestContext = struct {220test "Mutex - many uncontended" {
250 mutex: *Mutex,221 // This test requires spawning threads.
251 data: i128,222 if (builtin.single_threaded) {
223 return error.SkipZigTest;
224 }
225
226 const num_threads = 4;
227 const num_increments = 1000;
252228
253 const incr_count = 10000;229 const Runner = struct {
254};230 mutex: Mutex = .{},
231 thread: std.Thread = undefined,
232 counter: NonAtomicCounter = .{},
255233
256test "basic usage" {234 fn run(self: *@This()) void {
257 var mutex = Mutex{};235 var i: usize = num_increments;
236 while (i > 0) : (i -= 1) {
237 self.mutex.lock();
238 defer self.mutex.unlock();
258239
259 var context = TestContext{240 self.counter.inc();
260 .mutex = &mutex,241 }
261 .data = 0,242 }
262 };243 };
263244
245 var runners = [_]Runner{.{}} ** num_threads;
246 for (runners) |*r| r.thread = try std.Thread.spawn(.{}, Runner.run, .{r});
247 for (runners) |r| r.thread.join();
248 for (runners) |r| try testing.expectEqual(r.counter.get(), num_increments);
249}
250
251test "Mutex - many contended" {
252 // This test requires spawning threads.
264 if (builtin.single_threaded) {253 if (builtin.single_threaded) {
265 worker(&context);254 return error.SkipZigTest;
266 try testing.expect(context.data == TestContext.incr_count);255 }
267 } else {256
268 const thread_count = 10;257 const num_threads = 4;
269 var threads: [thread_count]std.Thread = undefined;258 const num_increments = 1000;
270 for (threads) |*t| {259
271 t.* = try std.Thread.spawn(.{}, worker, .{&context});260 const Runner = struct {
261 mutex: Mutex = .{},
262 counter: NonAtomicCounter = .{},
263
264 fn run(self: *@This()) void {
265 var i: usize = num_increments;
266 while (i > 0) : (i -= 1) {
267 // Occasionally hint to let another thread run.
268 defer if (i % 100 == 0) std.Thread.yield() catch {};
269
270 self.mutex.lock();
271 defer self.mutex.unlock();
272
273 self.counter.inc();
274 }
272 }275 }
273 for (threads) |t|276 };
274 t.join();
275277
276 try testing.expect(context.data == thread_count * TestContext.incr_count);278 var runner = Runner{};
277 }
278}
279279
280fn worker(ctx: *TestContext) void {280 var threads: [num_threads]std.Thread = undefined;
281 var i: usize = 0;281 for (threads) |*t| t.* = try std.Thread.spawn(.{}, Runner.run, .{&runner});
282 while (i != TestContext.incr_count) : (i += 1) {282 for (threads) |t| t.join();
283 ctx.mutex.lock();
284 defer ctx.mutex.unlock();
285283
286 ctx.data += 1;284 try testing.expectEqual(runner.counter.get(), num_increments * num_threads);
287 }
288}285}
lib/std/atomic/Atomic.zig+87-81
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1const std = @import("../std.zig");1const std = @import("../std.zig");
2const builtin = @import("builtin");
23
3const testing = std.testing;4const testing = std.testing;
4const target = @import("builtin").target;
5const Ordering = std.atomic.Ordering;5const Ordering = std.atomic.Ordering;
66
7pub fn Atomic(comptime T: type) type {7pub fn Atomic(comptime T: type) type {
...@@ -164,87 +164,13 @@ pub fn Atomic(comptime T: type) type {...@@ -164,87 +164,13 @@ pub fn Atomic(comptime T: type) type {
164 return bitRmw(self, .Toggle, bit, ordering);164 return bitRmw(self, .Toggle, bit, ordering);
165 }165 }
166166
167 inline fn bitRmw(167 inline fn bitRmw(self: *Self, comptime op: BitRmwOp, bit: Bit, comptime ordering: Ordering) u1 {
168 self: *Self,
169 comptime op: BitRmwOp,
170 bit: Bit,
171 comptime ordering: Ordering,
172 ) u1 {
173 // x86 supports dedicated bitwise instructions168 // x86 supports dedicated bitwise instructions
174 if (comptime target.cpu.arch.isX86() and @sizeOf(T) >= 2 and @sizeOf(T) <= 8) {169 if (comptime builtin.target.cpu.arch.isX86() and @sizeOf(T) >= 2 and @sizeOf(T) <= 8) {
175 const old_bit: u8 = switch (@sizeOf(T)) {170 // TODO: stage2 currently doesn't like the inline asm this function emits.
176 2 => switch (op) {171 if (builtin.zig_backend == .stage1) {
177 .Set => asm volatile ("lock btsw %[bit], %[ptr]"172 return x86BitRmw(self, op, bit, ordering);
178 // LLVM doesn't support u1 flag register return values173 }
179 : [result] "={@ccc}" (-> u8),
180 : [ptr] "*p" (&self.value),
181 [bit] "X" (@as(T, bit)),
182 : "cc", "memory"
183 ),
184 .Reset => asm volatile ("lock btrw %[bit], %[ptr]"
185 // LLVM doesn't support u1 flag register return values
186 : [result] "={@ccc}" (-> u8),
187 : [ptr] "*p" (&self.value),
188 [bit] "X" (@as(T, bit)),
189 : "cc", "memory"
190 ),
191 .Toggle => asm volatile ("lock btcw %[bit], %[ptr]"
192 // LLVM doesn't support u1 flag register return values
193 : [result] "={@ccc}" (-> u8),
194 : [ptr] "*p" (&self.value),
195 [bit] "X" (@as(T, bit)),
196 : "cc", "memory"
197 ),
198 },
199 4 => switch (op) {
200 .Set => asm volatile ("lock btsl %[bit], %[ptr]"
201 // LLVM doesn't support u1 flag register return values
202 : [result] "={@ccc}" (-> u8),
203 : [ptr] "*p" (&self.value),
204 [bit] "X" (@as(T, bit)),
205 : "cc", "memory"
206 ),
207 .Reset => asm volatile ("lock btrl %[bit], %[ptr]"
208 // LLVM doesn't support u1 flag register return values
209 : [result] "={@ccc}" (-> u8),
210 : [ptr] "*p" (&self.value),
211 [bit] "X" (@as(T, bit)),
212 : "cc", "memory"
213 ),
214 .Toggle => asm volatile ("lock btcl %[bit], %[ptr]"
215 // LLVM doesn't support u1 flag register return values
216 : [result] "={@ccc}" (-> u8),
217 : [ptr] "*p" (&self.value),
218 [bit] "X" (@as(T, bit)),
219 : "cc", "memory"
220 ),
221 },
222 8 => switch (op) {
223 .Set => asm volatile ("lock btsq %[bit], %[ptr]"
224 // LLVM doesn't support u1 flag register return values
225 : [result] "={@ccc}" (-> u8),
226 : [ptr] "*p" (&self.value),
227 [bit] "X" (@as(T, bit)),
228 : "cc", "memory"
229 ),
230 .Reset => asm volatile ("lock btrq %[bit], %[ptr]"
231 // LLVM doesn't support u1 flag register return values
232 : [result] "={@ccc}" (-> u8),
233 : [ptr] "*p" (&self.value),
234 [bit] "X" (@as(T, bit)),
235 : "cc", "memory"
236 ),
237 .Toggle => asm volatile ("lock btcq %[bit], %[ptr]"
238 // LLVM doesn't support u1 flag register return values
239 : [result] "={@ccc}" (-> u8),
240 : [ptr] "*p" (&self.value),
241 [bit] "X" (@as(T, bit)),
242 : "cc", "memory"
243 ),
244 },
245 else => @compileError("Invalid atomic type " ++ @typeName(T)),
246 };
247 return @intCast(u1, old_bit);
248 }174 }
249175
250 const mask = @as(T, 1) << bit;176 const mask = @as(T, 1) << bit;
...@@ -256,6 +182,86 @@ pub fn Atomic(comptime T: type) type {...@@ -256,6 +182,86 @@ pub fn Atomic(comptime T: type) type {
256182
257 return @boolToInt(value & mask != 0);183 return @boolToInt(value & mask != 0);
258 }184 }
185
186 inline fn x86BitRmw(self: *Self, comptime op: BitRmwOp, bit: Bit, comptime ordering: Ordering) u1 {
187 const old_bit: u8 = switch (@sizeOf(T)) {
188 2 => switch (op) {
189 .Set => asm volatile ("lock btsw %[bit], %[ptr]"
190 // LLVM doesn't support u1 flag register return values
191 : [result] "={@ccc}" (-> u8),
192 : [ptr] "*p" (&self.value),
193 [bit] "X" (@as(T, bit)),
194 : "cc", "memory"
195 ),
196 .Reset => asm volatile ("lock btrw %[bit], %[ptr]"
197 // LLVM doesn't support u1 flag register return values
198 : [result] "={@ccc}" (-> u8),
199 : [ptr] "*p" (&self.value),
200 [bit] "X" (@as(T, bit)),
201 : "cc", "memory"
202 ),
203 .Toggle => asm volatile ("lock btcw %[bit], %[ptr]"
204 // LLVM doesn't support u1 flag register return values
205 : [result] "={@ccc}" (-> u8),
206 : [ptr] "*p" (&self.value),
207 [bit] "X" (@as(T, bit)),
208 : "cc", "memory"
209 ),
210 },
211 4 => switch (op) {
212 .Set => asm volatile ("lock btsl %[bit], %[ptr]"
213 // LLVM doesn't support u1 flag register return values
214 : [result] "={@ccc}" (-> u8),
215 : [ptr] "*p" (&self.value),
216 [bit] "X" (@as(T, bit)),
217 : "cc", "memory"
218 ),
219 .Reset => asm volatile ("lock btrl %[bit], %[ptr]"
220 // LLVM doesn't support u1 flag register return values
221 : [result] "={@ccc}" (-> u8),
222 : [ptr] "*p" (&self.value),
223 [bit] "X" (@as(T, bit)),
224 : "cc", "memory"
225 ),
226 .Toggle => asm volatile ("lock btcl %[bit], %[ptr]"
227 // LLVM doesn't support u1 flag register return values
228 : [result] "={@ccc}" (-> u8),
229 : [ptr] "*p" (&self.value),
230 [bit] "X" (@as(T, bit)),
231 : "cc", "memory"
232 ),
233 },
234 8 => switch (op) {
235 .Set => asm volatile ("lock btsq %[bit], %[ptr]"
236 // LLVM doesn't support u1 flag register return values
237 : [result] "={@ccc}" (-> u8),
238 : [ptr] "*p" (&self.value),
239 [bit] "X" (@as(T, bit)),
240 : "cc", "memory"
241 ),
242 .Reset => asm volatile ("lock btrq %[bit], %[ptr]"
243 // LLVM doesn't support u1 flag register return values
244 : [result] "={@ccc}" (-> u8),
245 : [ptr] "*p" (&self.value),
246 [bit] "X" (@as(T, bit)),
247 : "cc", "memory"
248 ),
249 .Toggle => asm volatile ("lock btcq %[bit], %[ptr]"
250 // LLVM doesn't support u1 flag register return values
251 : [result] "={@ccc}" (-> u8),
252 : [ptr] "*p" (&self.value),
253 [bit] "X" (@as(T, bit)),
254 : "cc", "memory"
255 ),
256 },
257 else => @compileError("Invalid atomic type " ++ @typeName(T)),
258 };
259
260 // TODO: emit appropriate tsan fence if compiling with tsan
261 _ = ordering;
262
263 return @intCast(u1, old_bit);
264 }
259 });265 });
260 };266 };
261}267}
lib/std/heap/general_purpose_allocator.zig+8-3
...@@ -151,12 +151,12 @@ pub const Config = struct {...@@ -151,12 +151,12 @@ pub const Config = struct {
151151
152 /// What type of mutex you'd like to use, for thread safety.152 /// What type of mutex you'd like to use, for thread safety.
153 /// when specfied, the mutex type must have the same shape as `std.Thread.Mutex` and153 /// when specfied, the mutex type must have the same shape as `std.Thread.Mutex` and
154 /// `std.Thread.Mutex.Dummy`, and have no required fields. Specifying this field causes154 /// `DummyMutex`, and have no required fields. Specifying this field causes
155 /// the `thread_safe` field to be ignored.155 /// the `thread_safe` field to be ignored.
156 ///156 ///
157 /// when null (default):157 /// when null (default):
158 /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled.158 /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled.
159 /// * the mutex type defaults to `std.Thread.Mutex.Dummy` otherwise.159 /// * the mutex type defaults to `DummyMutex` otherwise.
160 MutexType: ?type = null,160 MutexType: ?type = null,
161161
162 /// This is a temporary debugging trick you can use to turn segfaults into more helpful162 /// This is a temporary debugging trick you can use to turn segfaults into more helpful
...@@ -198,7 +198,12 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {...@@ -198,7 +198,12 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
198 else if (config.thread_safe)198 else if (config.thread_safe)
199 std.Thread.Mutex{}199 std.Thread.Mutex{}
200 else200 else
201 std.Thread.Mutex.Dummy{};201 DummyMutex{};
202
203 const DummyMutex = struct {
204 fn lock(_: *DummyMutex) void {}
205 fn unlock(_: *DummyMutex) void {}
206 };
202207
203 const stack_n = config.stack_trace_frames;208 const stack_n = config.stack_trace_frames;
204 const one_trace_size = @sizeOf(usize) * stack_n;209 const one_trace_size = @sizeOf(usize) * stack_n;
lib/std/os/windows.zig+9-4
...@@ -3680,10 +3680,15 @@ pub const OBJECT_NAME_INFORMATION = extern struct {...@@ -3680,10 +3680,15 @@ pub const OBJECT_NAME_INFORMATION = extern struct {
3680 Name: UNICODE_STRING,3680 Name: UNICODE_STRING,
3681};3681};
36823682
3683pub const SRWLOCK = usize;3683pub const SRWLOCK_INIT = SRWLOCK{};
3684pub const SRWLOCK_INIT: SRWLOCK = 0;3684pub const SRWLOCK = extern struct {
3685pub const CONDITION_VARIABLE = usize;3685 Ptr: ?PVOID = null,
3686pub const CONDITION_VARIABLE_INIT: CONDITION_VARIABLE = 0;3686};
3687
3688pub const CONDITION_VARIABLE_INIT = CONDITION_VARIABLE{};
3689pub const CONDITION_VARIABLE = extern struct {
3690 Ptr: ?PVOID = null,
3691};
36873692
3688pub const FILE_SKIP_COMPLETION_PORT_ON_SUCCESS = 0x1;3693pub const FILE_SKIP_COMPLETION_PORT_ON_SUCCESS = 0x1;
3689pub const FILE_SKIP_SET_EVENT_ON_HANDLE = 0x2;3694pub const FILE_SKIP_SET_EVENT_ON_HANDLE = 0x2;