mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 14:23:09 +00:00
Beside the new order being consistent with the ThreadPool API and making more sense, this shuffling allows to write the context argument type in terms of the startFn arguments, reducing the use of anytype (eg. less explicit casts when using comptime_int parameters, yay). Sorry for the breakage. Closes #8082
229 lines
7.4 KiB
Zig
229 lines
7.4 KiB
Zig
// SPDX-License-Identifier: MIT
|
|
// Copyright (c) 2015-2021 Zig Contributors
|
|
// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
|
|
// The MIT license requires this copyright notice to be included in all copies
|
|
// and substantial portions of the software.
|
|
|
|
//! Similar to `StaticResetEvent` but on `set()` it also (atomically) does `reset()`.
|
|
//! Unlike StaticResetEvent, `wait()` can only be called by one thread (MPSC-like).
|
|
//!
|
|
//! AutoResetEvent has 3 possible states:
|
|
//! - UNSET: the AutoResetEvent is currently unset
|
|
//! - SET: the AutoResetEvent was notified before a wait() was called
|
|
//! - <StaticResetEvent pointer>: there is an active waiter waiting for a notification.
|
|
//!
|
|
//! When attempting to wait:
|
|
//! if the event is unset, it registers a ResetEvent pointer to be notified when the event is set
|
|
//! if the event is already set, then it consumes the notification and resets the event.
|
|
//!
|
|
//! When attempting to notify:
|
|
//! if the event is unset, then we set the event
|
|
//! if theres a waiting ResetEvent, then we unset the event and notify the ResetEvent
|
|
//!
|
|
//! This ensures that the event is automatically reset after a wait() has been issued
|
|
//! and avoids the race condition when using StaticResetEvent in the following scenario:
|
|
//! thread 1 | thread 2
|
|
//! StaticResetEvent.wait() |
|
|
//! | StaticResetEvent.set()
|
|
//! | StaticResetEvent.set()
|
|
//! StaticResetEvent.reset() |
|
|
//! StaticResetEvent.wait() | (missed the second .set() notification above)
|
|
|
|
state: usize = UNSET,
|
|
|
|
const std = @import("../std.zig");
|
|
const builtin = @import("builtin");
|
|
const testing = std.testing;
|
|
const assert = std.debug.assert;
|
|
const StaticResetEvent = std.Thread.StaticResetEvent;
|
|
const AutoResetEvent = @This();
|
|
|
|
const UNSET = 0;
|
|
const SET = 1;
|
|
|
|
/// the minimum alignment for the `*StaticResetEvent` created by wait*()
|
|
const event_align = std.math.max(@alignOf(StaticResetEvent), 2);
|
|
|
|
pub fn wait(self: *AutoResetEvent) void {
|
|
self.waitFor(null) catch unreachable;
|
|
}
|
|
|
|
pub fn timedWait(self: *AutoResetEvent, timeout: u64) error{TimedOut}!void {
|
|
return self.waitFor(timeout);
|
|
}
|
|
|
|
fn waitFor(self: *AutoResetEvent, timeout: ?u64) error{TimedOut}!void {
|
|
// lazily initialized StaticResetEvent
|
|
var reset_event: StaticResetEvent align(event_align) = undefined;
|
|
var has_reset_event = false;
|
|
|
|
var state = @atomicLoad(usize, &self.state, .SeqCst);
|
|
while (true) {
|
|
// consume a notification if there is any
|
|
if (state == SET) {
|
|
@atomicStore(usize, &self.state, UNSET, .SeqCst);
|
|
return;
|
|
}
|
|
|
|
// check if theres currently a pending ResetEvent pointer already registered
|
|
if (state != UNSET) {
|
|
unreachable; // multiple waiting threads on the same AutoResetEvent
|
|
}
|
|
|
|
// lazily initialize the ResetEvent if it hasn't been already
|
|
if (!has_reset_event) {
|
|
has_reset_event = true;
|
|
reset_event = .{};
|
|
}
|
|
|
|
// Since the AutoResetEvent currently isnt set,
|
|
// try to register our ResetEvent on it to wait
|
|
// for a set() call from another thread.
|
|
if (@cmpxchgWeak(
|
|
usize,
|
|
&self.state,
|
|
UNSET,
|
|
@ptrToInt(&reset_event),
|
|
.SeqCst,
|
|
.SeqCst,
|
|
)) |new_state| {
|
|
state = new_state;
|
|
continue;
|
|
}
|
|
|
|
// if no timeout was specified, then just wait forever
|
|
const timeout_ns = timeout orelse {
|
|
reset_event.wait();
|
|
return;
|
|
};
|
|
|
|
// wait with a timeout and return if signalled via set()
|
|
switch (reset_event.timedWait(timeout_ns)) {
|
|
.event_set => return,
|
|
.timed_out => {},
|
|
}
|
|
|
|
// If we timed out, we need to transition the AutoResetEvent back to UNSET.
|
|
// If we don't, then when we return, a set() thread could observe a pointer to an invalid ResetEvent.
|
|
state = @cmpxchgStrong(
|
|
usize,
|
|
&self.state,
|
|
@ptrToInt(&reset_event),
|
|
UNSET,
|
|
.SeqCst,
|
|
.SeqCst,
|
|
) orelse return error.TimedOut;
|
|
|
|
// We didn't manage to unregister ourselves from the state.
|
|
if (state == SET) {
|
|
unreachable; // AutoResetEvent notified without waking up the waiting thread
|
|
} else if (state != UNSET) {
|
|
unreachable; // multiple waiting threads on the same AutoResetEvent observed when timing out
|
|
}
|
|
|
|
// This menas a set() thread saw our ResetEvent pointer, acquired it, and is trying to wake it up.
|
|
// We need to wait for it to wake up our ResetEvent before we can return and invalidate it.
|
|
// We don't return error.TimedOut here as it technically notified us while we were "timing out".
|
|
reset_event.wait();
|
|
return;
|
|
}
|
|
}
|
|
|
|
pub fn set(self: *AutoResetEvent) void {
|
|
var state = @atomicLoad(usize, &self.state, .SeqCst);
|
|
while (true) {
|
|
// If the AutoResetEvent is already set, there is nothing else left to do
|
|
if (state == SET) {
|
|
return;
|
|
}
|
|
|
|
// If the AutoResetEvent isn't set,
|
|
// then try to leave a notification for the wait() thread that we set() it.
|
|
if (state == UNSET) {
|
|
state = @cmpxchgWeak(
|
|
usize,
|
|
&self.state,
|
|
UNSET,
|
|
SET,
|
|
.SeqCst,
|
|
.SeqCst,
|
|
) orelse return;
|
|
continue;
|
|
}
|
|
|
|
// There is a ResetEvent pointer registered on the AutoResetEvent event thats waiting.
|
|
// Try to acquire ownership of it so that we can wake it up.
|
|
// This also resets the AutoResetEvent so that there is no race condition as defined above.
|
|
if (@cmpxchgWeak(
|
|
usize,
|
|
&self.state,
|
|
state,
|
|
UNSET,
|
|
.SeqCst,
|
|
.SeqCst,
|
|
)) |new_state| {
|
|
state = new_state;
|
|
continue;
|
|
}
|
|
|
|
const reset_event = @intToPtr(*align(event_align) StaticResetEvent, state);
|
|
reset_event.set();
|
|
return;
|
|
}
|
|
}
|
|
|
|
test "basic usage" {
|
|
// test local code paths
|
|
{
|
|
var event = AutoResetEvent{};
|
|
testing.expectError(error.TimedOut, event.timedWait(1));
|
|
event.set();
|
|
event.wait();
|
|
}
|
|
|
|
// test cross-thread signaling
|
|
if (builtin.single_threaded)
|
|
return;
|
|
|
|
const Context = struct {
|
|
value: u128 = 0,
|
|
in: AutoResetEvent = AutoResetEvent{},
|
|
out: AutoResetEvent = AutoResetEvent{},
|
|
|
|
const Self = @This();
|
|
|
|
fn sender(self: *Self) void {
|
|
testing.expect(self.value == 0);
|
|
self.value = 1;
|
|
self.out.set();
|
|
|
|
self.in.wait();
|
|
testing.expect(self.value == 2);
|
|
self.value = 3;
|
|
self.out.set();
|
|
|
|
self.in.wait();
|
|
testing.expect(self.value == 4);
|
|
}
|
|
|
|
fn receiver(self: *Self) void {
|
|
self.out.wait();
|
|
testing.expect(self.value == 1);
|
|
self.value = 2;
|
|
self.in.set();
|
|
|
|
self.out.wait();
|
|
testing.expect(self.value == 3);
|
|
self.value = 4;
|
|
self.in.set();
|
|
}
|
|
};
|
|
|
|
var context = Context{};
|
|
const send_thread = try std.Thread.spawn(Context.sender, &context);
|
|
const recv_thread = try std.Thread.spawn(Context.receiver, &context);
|
|
|
|
send_thread.wait();
|
|
recv_thread.wait();
|
|
}
|