organize std lib concurrency primitives and add RwLock

* move concurrency primitives that always operate on kernel threads to
   the std.Thread namespace
 * remove std.SpinLock. Nobody should use this in a non-freestanding
   environment; the other primitives are always preferable. In
   freestanding, it will be necessary to put custom spin logic in there,
   so there are no use cases for a std lib version.
 * move some std lib files to the top level fields convention
 * add std.Thread.spinLoopHint
 * add std.Thread.Condition
 * add std.Thread.Semaphore
 * new implementation of std.Thread.Mutex for Windows and non-pthreads Linux
 * add std.Thread.RwLock

Implementations provided by @kprotty
This commit is contained in:
Andrew Kelley 2021-01-14 20:41:37 -07:00
parent 2b0e3ee228
commit a9667b5a85
38 changed files with 1756 additions and 1272 deletions

View File

@ -334,7 +334,6 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/atomic/int.zig" "${CMAKE_SOURCE_DIR}/lib/std/atomic/int.zig"
"${CMAKE_SOURCE_DIR}/lib/std/atomic/queue.zig" "${CMAKE_SOURCE_DIR}/lib/std/atomic/queue.zig"
"${CMAKE_SOURCE_DIR}/lib/std/atomic/stack.zig" "${CMAKE_SOURCE_DIR}/lib/std/atomic/stack.zig"
"${CMAKE_SOURCE_DIR}/lib/std/auto_reset_event.zig"
"${CMAKE_SOURCE_DIR}/lib/std/base64.zig" "${CMAKE_SOURCE_DIR}/lib/std/base64.zig"
"${CMAKE_SOURCE_DIR}/lib/std/buf_map.zig" "${CMAKE_SOURCE_DIR}/lib/std/buf_map.zig"
"${CMAKE_SOURCE_DIR}/lib/std/builtin.zig" "${CMAKE_SOURCE_DIR}/lib/std/builtin.zig"
@ -409,7 +408,6 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/meta.zig" "${CMAKE_SOURCE_DIR}/lib/std/meta.zig"
"${CMAKE_SOURCE_DIR}/lib/std/meta/trailer_flags.zig" "${CMAKE_SOURCE_DIR}/lib/std/meta/trailer_flags.zig"
"${CMAKE_SOURCE_DIR}/lib/std/meta/trait.zig" "${CMAKE_SOURCE_DIR}/lib/std/meta/trait.zig"
"${CMAKE_SOURCE_DIR}/lib/std/mutex.zig"
"${CMAKE_SOURCE_DIR}/lib/std/os.zig" "${CMAKE_SOURCE_DIR}/lib/std/os.zig"
"${CMAKE_SOURCE_DIR}/lib/std/os/bits.zig" "${CMAKE_SOURCE_DIR}/lib/std/os/bits.zig"
"${CMAKE_SOURCE_DIR}/lib/std/os/bits/linux.zig" "${CMAKE_SOURCE_DIR}/lib/std/os/bits/linux.zig"
@ -426,8 +424,6 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/os/windows/ntstatus.zig" "${CMAKE_SOURCE_DIR}/lib/std/os/windows/ntstatus.zig"
"${CMAKE_SOURCE_DIR}/lib/std/os/windows/win32error.zig" "${CMAKE_SOURCE_DIR}/lib/std/os/windows/win32error.zig"
"${CMAKE_SOURCE_DIR}/lib/std/Progress.zig" "${CMAKE_SOURCE_DIR}/lib/std/Progress.zig"
"${CMAKE_SOURCE_DIR}/lib/std/ResetEvent.zig"
"${CMAKE_SOURCE_DIR}/lib/std/StaticResetEvent.zig"
"${CMAKE_SOURCE_DIR}/lib/std/pdb.zig" "${CMAKE_SOURCE_DIR}/lib/std/pdb.zig"
"${CMAKE_SOURCE_DIR}/lib/std/process.zig" "${CMAKE_SOURCE_DIR}/lib/std/process.zig"
"${CMAKE_SOURCE_DIR}/lib/std/rand.zig" "${CMAKE_SOURCE_DIR}/lib/std/rand.zig"
@ -494,7 +490,6 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/udivmodti4.zig" "${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/udivmodti4.zig"
"${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/udivti3.zig" "${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/udivti3.zig"
"${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/umodti3.zig" "${CMAKE_SOURCE_DIR}/lib/std/special/compiler_rt/umodti3.zig"
"${CMAKE_SOURCE_DIR}/lib/std/SpinLock.zig"
"${CMAKE_SOURCE_DIR}/lib/std/start.zig" "${CMAKE_SOURCE_DIR}/lib/std/start.zig"
"${CMAKE_SOURCE_DIR}/lib/std/std.zig" "${CMAKE_SOURCE_DIR}/lib/std/std.zig"
"${CMAKE_SOURCE_DIR}/lib/std/target.zig" "${CMAKE_SOURCE_DIR}/lib/std/target.zig"
@ -513,7 +508,11 @@ set(ZIG_STAGE2_SOURCES
"${CMAKE_SOURCE_DIR}/lib/std/target/systemz.zig" "${CMAKE_SOURCE_DIR}/lib/std/target/systemz.zig"
"${CMAKE_SOURCE_DIR}/lib/std/target/wasm.zig" "${CMAKE_SOURCE_DIR}/lib/std/target/wasm.zig"
"${CMAKE_SOURCE_DIR}/lib/std/target/x86.zig" "${CMAKE_SOURCE_DIR}/lib/std/target/x86.zig"
"${CMAKE_SOURCE_DIR}/lib/std/thread.zig" "${CMAKE_SOURCE_DIR}/lib/std/Thread.zig"
"${CMAKE_SOURCE_DIR}/lib/std/Thread/AutoResetEvent.zig"
"${CMAKE_SOURCE_DIR}/lib/std/Thread/Mutex.zig"
"${CMAKE_SOURCE_DIR}/lib/std/Thread/ResetEvent.zig"
"${CMAKE_SOURCE_DIR}/lib/std/Thread/StaticResetEvent.zig"
"${CMAKE_SOURCE_DIR}/lib/std/time.zig" "${CMAKE_SOURCE_DIR}/lib/std/time.zig"
"${CMAKE_SOURCE_DIR}/lib/std/unicode.zig" "${CMAKE_SOURCE_DIR}/lib/std/unicode.zig"
"${CMAKE_SOURCE_DIR}/lib/std/zig.zig" "${CMAKE_SOURCE_DIR}/lib/std/zig.zig"

View File

@ -50,7 +50,7 @@ done: bool = true,
/// Protects the `refresh` function, as well as `node.recently_updated_child`. /// Protects the `refresh` function, as well as `node.recently_updated_child`.
/// Without this, callsites would call `Node.end` and then free `Node` memory /// Without this, callsites would call `Node.end` and then free `Node` memory
/// while it was still being accessed by the `refresh` function. /// while it was still being accessed by the `refresh` function.
update_lock: std.Mutex = .{}, update_lock: std.Thread.Mutex = .{},
/// Keeps track of how many columns in the terminal have been output, so that /// Keeps track of how many columns in the terminal have been output, so that
/// we can move the cursor back later. /// we can move the cursor back later.

View File

@ -1,86 +0,0 @@
// 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.
//! A mutually exclusive lock that grinds the CPU rather than interacting with
//! the operating system. It does however yield to the OS scheduler while
//! spinning, when targeting an OS that supports it.
//! This struct can be initialized directly and statically initialized. The
//! default state is unlocked.
state: State = State.Unlocked,
const std = @import("std.zig");
const builtin = @import("builtin");
const SpinLock = @This();
const State = enum(u8) {
Unlocked,
Locked,
};
pub const Held = struct {
spinlock: *SpinLock,
pub fn release(self: Held) void {
@atomicStore(State, &self.spinlock.state, .Unlocked, .Release);
}
};
pub fn tryAcquire(self: *SpinLock) ?Held {
return switch (@atomicRmw(State, &self.state, .Xchg, .Locked, .Acquire)) {
.Unlocked => Held{ .spinlock = self },
.Locked => null,
};
}
pub fn acquire(self: *SpinLock) Held {
while (true) {
return self.tryAcquire() orelse {
yield();
continue;
};
}
}
pub fn yield() void {
// On native windows, SwitchToThread is too expensive,
// and yielding for 380-410 iterations was found to be
// a nice sweet spot. Posix systems on the other hand,
// especially linux, perform better by yielding the thread.
switch (builtin.os.tag) {
.windows => loopHint(400),
else => std.os.sched_yield() catch loopHint(1),
}
}
/// Hint to the cpu that execution is spinning
/// for the given amount of iterations.
pub fn loopHint(iterations: usize) void {
var i = iterations;
while (i != 0) : (i -= 1) {
switch (builtin.arch) {
// these instructions use a memory clobber as they
// flush the pipeline of any speculated reads/writes.
.i386, .x86_64 => asm volatile ("pause"
:
:
: "memory"
),
.arm, .aarch64 => asm volatile ("yield"
:
:
: "memory"
),
else => std.os.sched_yield() catch {},
}
}
}
test "basic usage" {
var lock: SpinLock = .{};
const held = lock.acquire();
defer held.release();
}

558
lib/std/Thread.zig Normal file
View File

@ -0,0 +1,558 @@
// 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.
//! This struct represents a kernel thread, and acts as a namespace for concurrency
//! primitives that operate on kernel threads. For concurrency primitives that support
//! both evented I/O and async I/O, see the respective names in the top level std namespace.
data: Data,
pub const AutoResetEvent = @import("Thread/AutoResetEvent.zig");
pub const ResetEvent = @import("Thread/ResetEvent.zig");
pub const StaticResetEvent = @import("Thread/StaticResetEvent.zig");
pub const Mutex = @import("Thread/Mutex.zig");
pub const Semaphore = @import("Thread/Semaphore.zig");
pub const Condition = @import("Thread/Condition.zig");
pub const use_pthreads = std.Target.current.os.tag != .windows and builtin.link_libc;
const Thread = @This();
const std = @import("std.zig");
const builtin = std.builtin;
const os = std.os;
const mem = std.mem;
const windows = std.os.windows;
const c = std.c;
const assert = std.debug.assert;
const bad_startfn_ret = "expected return type of startFn to be 'u8', 'noreturn', 'void', or '!void'";
/// Represents a kernel thread handle.
/// May be an integer or a pointer depending on the platform.
/// On Linux and POSIX, this is the same as Id.
pub const Handle = if (use_pthreads)
c.pthread_t
else switch (std.Target.current.os.tag) {
.linux => i32,
.windows => windows.HANDLE,
else => void,
};
/// Represents a unique ID per thread.
/// May be an integer or pointer depending on the platform.
/// On Linux and POSIX, this is the same as Handle.
pub const Id = switch (std.Target.current.os.tag) {
.windows => windows.DWORD,
else => Handle,
};
pub const Data = if (use_pthreads)
struct {
handle: Thread.Handle,
memory: []u8,
}
else switch (std.Target.current.os.tag) {
.linux => struct {
handle: Thread.Handle,
memory: []align(mem.page_size) u8,
},
.windows => struct {
handle: Thread.Handle,
alloc_start: *c_void,
heap_handle: windows.HANDLE,
},
else => struct {},
};
/// Signals the processor that it is inside a busy-wait spin-loop ("spin lock").
pub fn spinLoopHint() void {
switch (std.Target.current.cpu.arch) {
.i386, .x86_64 => asm volatile ("pause"
:
:
: "memory"
),
.arm, .aarch64 => asm volatile ("yield"
:
:
: "memory"
),
else => {},
}
}
/// Returns the ID of the calling thread.
/// Makes a syscall every time the function is called.
/// On Linux and POSIX, this Id is the same as a Handle.
pub fn getCurrentId() Id {
if (use_pthreads) {
return c.pthread_self();
} else
return switch (std.Target.current.os.tag) {
.linux => os.linux.gettid(),
.windows => windows.kernel32.GetCurrentThreadId(),
else => @compileError("Unsupported OS"),
};
}
/// Returns the handle of this thread.
/// On Linux and POSIX, this is the same as Id.
/// On Linux, it is possible that the thread spawned with `spawn`
/// finishes executing entirely before the clone syscall completes. In this
/// case, this function will return 0 rather than the no-longer-existing thread's
/// pid.
pub fn handle(self: Thread) Handle {
return self.data.handle;
}
pub fn wait(self: *Thread) void {
if (use_pthreads) {
const err = c.pthread_join(self.data.handle, null);
switch (err) {
0 => {},
os.EINVAL => unreachable,
os.ESRCH => unreachable,
os.EDEADLK => unreachable,
else => unreachable,
}
std.heap.c_allocator.free(self.data.memory);
std.heap.c_allocator.destroy(self);
} else switch (std.Target.current.os.tag) {
.linux => {
while (true) {
const pid_value = @atomicLoad(i32, &self.data.handle, .SeqCst);
if (pid_value == 0) break;
const rc = os.linux.futex_wait(&self.data.handle, os.linux.FUTEX_WAIT, pid_value, null);
switch (os.linux.getErrno(rc)) {
0 => continue,
os.EINTR => continue,
os.EAGAIN => continue,
else => unreachable,
}
}
os.munmap(self.data.memory);
},
.windows => {
windows.WaitForSingleObjectEx(self.data.handle, windows.INFINITE, false) catch unreachable;
windows.CloseHandle(self.data.handle);
windows.HeapFree(self.data.heap_handle, 0, self.data.alloc_start);
},
else => @compileError("Unsupported OS"),
}
}
pub const SpawnError = error{
/// A system-imposed limit on the number of threads was encountered.
/// There are a number of limits that may trigger this error:
/// * the RLIMIT_NPROC soft resource limit (set via setrlimit(2)),
/// which limits the number of processes and threads for a real
/// user ID, was reached;
/// * the kernel's system-wide limit on the number of processes and
/// threads, /proc/sys/kernel/threads-max, was reached (see
/// proc(5));
/// * the maximum number of PIDs, /proc/sys/kernel/pid_max, was
/// reached (see proc(5)); or
/// * the PID limit (pids.max) imposed by the cgroup "process num
/// ber" (PIDs) controller was reached.
ThreadQuotaExceeded,
/// The kernel cannot allocate sufficient memory to allocate a task structure
/// for the child, or to copy those parts of the caller's context that need to
/// be copied.
SystemResources,
/// Not enough userland memory to spawn the thread.
OutOfMemory,
/// `mlockall` is enabled, and the memory needed to spawn the thread
/// would exceed the limit.
LockedMemoryLimitExceeded,
Unexpected,
};
/// caller must call wait on the returned thread
/// fn startFn(@TypeOf(context)) T
/// where T is u8, noreturn, void, or !void
/// caller must call wait on the returned thread
pub fn spawn(context: anytype, comptime startFn: anytype) SpawnError!*Thread {
if (builtin.single_threaded) @compileError("cannot spawn thread when building in single-threaded mode");
// TODO compile-time call graph analysis to determine stack upper bound
// https://github.com/ziglang/zig/issues/157
const default_stack_size = 16 * 1024 * 1024;
const Context = @TypeOf(context);
comptime assert(@typeInfo(@TypeOf(startFn)).Fn.args[0].arg_type.? == Context);
if (std.Target.current.os.tag == .windows) {
const WinThread = struct {
const OuterContext = struct {
thread: Thread,
inner: Context,
};
fn threadMain(raw_arg: windows.LPVOID) callconv(.C) windows.DWORD {
const arg = if (@sizeOf(Context) == 0) {} else @ptrCast(*Context, @alignCast(@alignOf(Context), raw_arg)).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return 0;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
return startFn(arg);
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return 0;
},
else => @compileError(bad_startfn_ret),
}
}
};
const heap_handle = windows.kernel32.GetProcessHeap() orelse return error.OutOfMemory;
const byte_count = @alignOf(WinThread.OuterContext) + @sizeOf(WinThread.OuterContext);
const bytes_ptr = windows.kernel32.HeapAlloc(heap_handle, 0, byte_count) orelse return error.OutOfMemory;
errdefer assert(windows.kernel32.HeapFree(heap_handle, 0, bytes_ptr) != 0);
const bytes = @ptrCast([*]u8, bytes_ptr)[0..byte_count];
const outer_context = std.heap.FixedBufferAllocator.init(bytes).allocator.create(WinThread.OuterContext) catch unreachable;
outer_context.* = WinThread.OuterContext{
.thread = Thread{
.data = Thread.Data{
.heap_handle = heap_handle,
.alloc_start = bytes_ptr,
.handle = undefined,
},
},
.inner = context,
};
const parameter = if (@sizeOf(Context) == 0) null else @ptrCast(*c_void, &outer_context.inner);
outer_context.thread.data.handle = windows.kernel32.CreateThread(null, default_stack_size, WinThread.threadMain, parameter, 0, null) orelse {
switch (windows.kernel32.GetLastError()) {
else => |err| return windows.unexpectedError(err),
}
};
return &outer_context.thread;
}
const MainFuncs = struct {
fn linuxThreadMain(ctx_addr: usize) callconv(.C) u8 {
const arg = if (@sizeOf(Context) == 0) {} else @intToPtr(*const Context, ctx_addr).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return 0;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
return startFn(arg);
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return 0;
},
else => @compileError(bad_startfn_ret),
}
}
fn posixThreadMain(ctx: ?*c_void) callconv(.C) ?*c_void {
const arg = if (@sizeOf(Context) == 0) {} else @ptrCast(*Context, @alignCast(@alignOf(Context), ctx)).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return null;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
// pthreads don't support exit status, ignore value
_ = startFn(arg);
return null;
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return null;
},
else => @compileError(bad_startfn_ret),
}
}
};
if (Thread.use_pthreads) {
var attr: c.pthread_attr_t = undefined;
if (c.pthread_attr_init(&attr) != 0) return error.SystemResources;
defer assert(c.pthread_attr_destroy(&attr) == 0);
const thread_obj = try std.heap.c_allocator.create(Thread);
errdefer std.heap.c_allocator.destroy(thread_obj);
if (@sizeOf(Context) > 0) {
thread_obj.data.memory = try std.heap.c_allocator.allocAdvanced(
u8,
@alignOf(Context),
@sizeOf(Context),
.at_least,
);
errdefer std.heap.c_allocator.free(thread_obj.data.memory);
mem.copy(u8, thread_obj.data.memory, mem.asBytes(&context));
} else {
thread_obj.data.memory = @as([*]u8, undefined)[0..0];
}
// Use the same set of parameters used by the libc-less impl.
assert(c.pthread_attr_setstacksize(&attr, default_stack_size) == 0);
assert(c.pthread_attr_setguardsize(&attr, mem.page_size) == 0);
const err = c.pthread_create(
&thread_obj.data.handle,
&attr,
MainFuncs.posixThreadMain,
thread_obj.data.memory.ptr,
);
switch (err) {
0 => return thread_obj,
os.EAGAIN => return error.SystemResources,
os.EPERM => unreachable,
os.EINVAL => unreachable,
else => return os.unexpectedErrno(@intCast(usize, err)),
}
return thread_obj;
}
var guard_end_offset: usize = undefined;
var stack_end_offset: usize = undefined;
var thread_start_offset: usize = undefined;
var context_start_offset: usize = undefined;
var tls_start_offset: usize = undefined;
const mmap_len = blk: {
var l: usize = mem.page_size;
// Allocate a guard page right after the end of the stack region
guard_end_offset = l;
// The stack itself, which grows downwards.
l = mem.alignForward(l + default_stack_size, mem.page_size);
stack_end_offset = l;
// Above the stack, so that it can be in the same mmap call, put the Thread object.
l = mem.alignForward(l, @alignOf(Thread));
thread_start_offset = l;
l += @sizeOf(Thread);
// Next, the Context object.
if (@sizeOf(Context) != 0) {
l = mem.alignForward(l, @alignOf(Context));
context_start_offset = l;
l += @sizeOf(Context);
}
// Finally, the Thread Local Storage, if any.
l = mem.alignForward(l, os.linux.tls.tls_image.alloc_align);
tls_start_offset = l;
l += os.linux.tls.tls_image.alloc_size;
// Round the size to the page size.
break :blk mem.alignForward(l, mem.page_size);
};
const mmap_slice = mem: {
// Map the whole stack with no rw permissions to avoid
// committing the whole region right away
const mmap_slice = os.mmap(
null,
mmap_len,
os.PROT_NONE,
os.MAP_PRIVATE | os.MAP_ANONYMOUS,
-1,
0,
) catch |err| switch (err) {
error.MemoryMappingNotSupported => unreachable,
error.AccessDenied => unreachable,
error.PermissionDenied => unreachable,
else => |e| return e,
};
errdefer os.munmap(mmap_slice);
// Map everything but the guard page as rw
os.mprotect(
mmap_slice[guard_end_offset..],
os.PROT_READ | os.PROT_WRITE,
) catch |err| switch (err) {
error.AccessDenied => unreachable,
else => |e| return e,
};
break :mem mmap_slice;
};
const mmap_addr = @ptrToInt(mmap_slice.ptr);
const thread_ptr = @alignCast(@alignOf(Thread), @intToPtr(*Thread, mmap_addr + thread_start_offset));
thread_ptr.data.memory = mmap_slice;
var arg: usize = undefined;
if (@sizeOf(Context) != 0) {
arg = mmap_addr + context_start_offset;
const context_ptr = @alignCast(@alignOf(Context), @intToPtr(*Context, arg));
context_ptr.* = context;
}
if (std.Target.current.os.tag == .linux) {
const flags: u32 = os.CLONE_VM | os.CLONE_FS | os.CLONE_FILES |
os.CLONE_SIGHAND | os.CLONE_THREAD | os.CLONE_SYSVSEM |
os.CLONE_PARENT_SETTID | os.CLONE_CHILD_CLEARTID |
os.CLONE_DETACHED | os.CLONE_SETTLS;
// This structure is only needed when targeting i386
var user_desc: if (std.Target.current.cpu.arch == .i386) os.linux.user_desc else void = undefined;
const tls_area = mmap_slice[tls_start_offset..];
const tp_value = os.linux.tls.prepareTLS(tls_area);
const newtls = blk: {
if (std.Target.current.cpu.arch == .i386) {
user_desc = os.linux.user_desc{
.entry_number = os.linux.tls.tls_image.gdt_entry_number,
.base_addr = tp_value,
.limit = 0xfffff,
.seg_32bit = 1,
.contents = 0, // Data
.read_exec_only = 0,
.limit_in_pages = 1,
.seg_not_present = 0,
.useable = 1,
};
break :blk @ptrToInt(&user_desc);
} else {
break :blk tp_value;
}
};
const rc = os.linux.clone(
MainFuncs.linuxThreadMain,
mmap_addr + stack_end_offset,
flags,
arg,
&thread_ptr.data.handle,
newtls,
&thread_ptr.data.handle,
);
switch (os.errno(rc)) {
0 => return thread_ptr,
os.EAGAIN => return error.ThreadQuotaExceeded,
os.EINVAL => unreachable,
os.ENOMEM => return error.SystemResources,
os.ENOSPC => unreachable,
os.EPERM => unreachable,
os.EUSERS => unreachable,
else => |err| return os.unexpectedErrno(err),
}
} else {
@compileError("Unsupported OS");
}
}
pub const CpuCountError = error{
PermissionDenied,
SystemResources,
Unexpected,
};
pub fn cpuCount() CpuCountError!usize {
if (std.Target.current.os.tag == .linux) {
const cpu_set = try os.sched_getaffinity(0);
return @as(usize, os.CPU_COUNT(cpu_set)); // TODO should not need this usize cast
}
if (std.Target.current.os.tag == .windows) {
return os.windows.peb().NumberOfProcessors;
}
if (std.Target.current.os.tag == .openbsd) {
var count: c_int = undefined;
var count_size: usize = @sizeOf(c_int);
const mib = [_]c_int{ os.CTL_HW, os.HW_NCPUONLINE };
os.sysctl(&mib, &count, &count_size, null, 0) catch |err| switch (err) {
error.NameTooLong, error.UnknownName => unreachable,
else => |e| return e,
};
return @intCast(usize, count);
}
var count: c_int = undefined;
var count_len: usize = @sizeOf(c_int);
const name = if (comptime std.Target.current.isDarwin()) "hw.logicalcpu" else "hw.ncpu";
os.sysctlbynameZ(name, &count, &count_len, null, 0) catch |err| switch (err) {
error.NameTooLong, error.UnknownName => unreachable,
else => |e| return e,
};
return @intCast(usize, count);
}
pub fn getCurrentThreadId() u64 {
switch (std.Target.current.os.tag) {
.linux => {
// Use the syscall directly as musl doesn't provide a wrapper.
return @bitCast(u32, os.linux.gettid());
},
.windows => {
return os.windows.kernel32.GetCurrentThreadId();
},
.macos, .ios, .watchos, .tvos => {
var thread_id: u64 = undefined;
// Pass thread=null to get the current thread ID.
assert(c.pthread_threadid_np(null, &thread_id) == 0);
return thread_id;
},
.netbsd => {
return @bitCast(u32, c._lwp_self());
},
.freebsd => {
return @bitCast(u32, c.pthread_getthreadid_np());
},
.openbsd => {
return @bitCast(u32, c.getthrid());
},
else => {
@compileError("getCurrentThreadId not implemented for this platform");
},
}
}
test "" {
std.testing.refAllDecls(@This());
}

View File

@ -0,0 +1,228 @@
// 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, Context.sender);
const recv_thread = try std.Thread.spawn(&context, Context.receiver);
send_thread.wait();
recv_thread.wait();
}

View File

@ -0,0 +1,182 @@
// 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.
//! A condition provides a way for a kernel thread to block until it is signaled
//! to wake up. Spurious wakeups are possible.
//! This API supports static initialization and does not require deinitialization.
impl: Impl,
const std = @import("../std.zig");
const Condition = @This();
const windows = std.os.windows;
const linux = std.os.linux;
const Mutex = std.Thread.Mutex;
const assert = std.debug.assert;
const Impl = if (std.builtin.single_threaded)
SingleThreadedCondition
else if (std.Target.current.os.tag == .windows)
WindowsCondition
else if (std.Thread.use_pthreads)
PthreadCondition
else
AtomicCondition;
pub const SingleThreadedCondition = struct {
pub fn wait(cond: *SingleThreadedCondition, mutex: *Mutex) void {
unreachable; // deadlock detected
}
pub fn signal(cond: *SingleThreadedCondition) void {}
pub fn broadcast(cond: *SingleThreadedCondition) void {}
};
pub const WindowsCondition = struct {
cond: windows.CONDITION_VARIABLE = windows.CONDITION_VARIABLE_INIT,
pub fn wait(cond: *WindowsCondition, mutex: *Mutex) void {
const rc = windows.SleepConditionVariableSRW(
&cond.cond,
&mutex.srwlock,
windows.INFINITE,
@as(windows.ULONG, 0),
);
assert(rc != windows.FALSE);
}
pub fn signal(cond: *WindowsCondition) void {
windows.WakeConditionVariable(&cond.cond);
}
pub fn broadcast(cond: *WindowsCondition) void {
windows.WakeAllConditionVariable(&cond.cond);
}
};
pub const PthreadCondition = struct {
cond: std.c.pthread_cond_t = .{},
pub fn wait(cond: *PthreadCondition, mutex: *Mutex) void {
const rc = std.c.pthread_cond_wait(&cond.cond, &mutex.mutex);
assert(rc == 0);
}
pub fn signal(cond: *PthreadCondition) void {
const rc = std.c.pthread_cond_signal(&cond.cond);
assert(rc == 0);
}
pub fn broadcast(cond: *PthreadCondition) void {
const rc = std.c.pthread_cond_broadcast(&cond.cond);
assert(rc == 0);
}
};
pub const AtomicCondition = struct {
pending: bool = false,
queue_mutex: Mutex = .{},
queue_list: QueueList = .{},
pub const QueueList = std.SinglyLinkedList(QueueItem);
pub const QueueItem = struct {
futex: i32 = 0,
fn wait(cond: *@This()) void {
while (@atomicLoad(i32, &cond.futex, .Acquire) == 0) {
switch (std.Target.current.os.tag) {
.linux => {
switch (linux.getErrno(linux.futex_wait(
&cond.futex,
linux.FUTEX_PRIVATE_FLAG | linux.FUTEX_WAIT,
0,
null,
))) {
0 => {},
std.os.EINTR => {},
std.os.EAGAIN => {},
else => unreachable,
}
},
else => spinLoopHint(),
}
}
}
fn notify(cond: *@This()) void {
@atomicStore(i32, &cond.futex, 1, .Release);
switch (std.Target.current.os.tag) {
.linux => {
switch (linux.getErrno(linux.futex_wake(
&cond.futex,
linux.FUTEX_PRIVATE_FLAG | linux.FUTEX_WAKE,
1,
))) {
0 => {},
std.os.EFAULT => {},
else => unreachable,
}
},
else => {},
}
}
};
pub fn wait(cond: *AtomicCondition, mutex: *Mutex) void {
var waiter = QueueList.Node{ .data = .{} };
{
const held = cond.queue_mutex.acquire();
defer held.release();
cond.queue_list.prepend(&waiter);
@atomicStore(bool, &cond.pending, true, .SeqCst);
}
mutex.unlock();
waiter.data.wait();
mutex.lock();
}
pub fn signal(cond: *AtomicCondition) void {
if (@atomicLoad(bool, &cond.pending, .SeqCst) == false)
return;
const maybe_waiter = blk: {
const held = cond.queue_mutex.acquire();
defer held.release();
const maybe_waiter = cond.queue_list.popFirst();
@atomicStore(bool, &cond.pending, cond.queue_list.first != null, .SeqCst);
break :blk maybe_waiter;
};
if (maybe_waiter) |waiter|
waiter.data.notify();
}
pub fn broadcast(cond: *AtomicCondition) void {
if (@atomicLoad(bool, &cond.pending, .SeqCst) == false)
return;
@atomicStore(bool, &cond.pending, false, .SeqCst);
var waiters = blk: {
const held = cond.queue_mutex.acquire();
defer held.release();
const waiters = cond.queue_list;
cond.queue_list = .{};
break :blk waiters;
};
while (waiters.popFirst()) |waiter|
waiter.data.notify();
}
};

303
lib/std/Thread/Mutex.zig Normal file
View File

@ -0,0 +1,303 @@
// 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.
//! Lock may be held only once. If the same thread tries to acquire
//! the same mutex twice, it deadlocks. This type supports static
//! initialization and is at most `@sizeOf(usize)` in size. When an
//! application is built in single threaded release mode, all the
//! functions are no-ops. In single threaded debug mode, there is
//! deadlock detection.
//!
//! Example usage:
//! var m = Mutex{};
//!
//! const lock = m.acquire();
//! defer lock.release();
//! ... critical code
//!
//! Non-blocking:
//! if (m.tryAcquire) |lock| {
//! defer lock.release();
//! // ... critical section
//! } else {
//! // ... lock not acquired
//! }
impl: Impl = .{},
const Mutex = @This();
const std = @import("../std.zig");
const builtin = std.builtin;
const os = std.os;
const assert = std.debug.assert;
const windows = os.windows;
const linux = os.linux;
const testing = std.testing;
const StaticResetEvent = std.thread.StaticResetEvent;
pub const Held = struct {
impl: *Impl,
pub fn release(held: Held) void {
held.impl.release();
}
};
/// Try to acquire the mutex without blocking. Returns null if
/// the mutex is unavailable. Otherwise returns Held. Call
/// release on Held.
pub fn tryAcquire(m: *Mutex) ?Held {
if (m.impl.tryAcquire()) {
return Held{ .impl = &m.impl };
} else {
return null;
}
}
/// Acquire the mutex. Deadlocks if the mutex is already
/// held by the calling thread.
pub fn acquire(m: *Mutex) Held {
m.impl.acquire();
return .{ .impl = &m.impl };
}
const Impl = if (builtin.single_threaded)
Dummy
else if (builtin.os.tag == .windows)
WindowsMutex
else if (std.Thread.use_pthreads)
PthreadMutex
else
AtomicMutex;
pub const AtomicMutex = struct {
state: State = .unlocked,
const State = enum(i32) {
unlocked,
locked,
waiting,
};
pub fn tryAcquire(self: *AtomicMutex) bool {
return @cmpxchgStrong(
State,
&self.state,
.unlocked,
.locked,
.Acquire,
.Monotonic,
) == null;
}
pub fn acquire(self: *AtomicMutex) void {
switch (@atomicRmw(State, &self.state, .Xchg, .locked, .Acquire)) {
.unlocked => {},
else => |s| self.lockSlow(s),
}
}
fn lockSlow(self: *AtomicMutex, current_state: State) void {
@setCold(true);
var new_state = current_state;
var spin: u8 = 0;
while (spin < 100) : (spin += 1) {
const state = @cmpxchgWeak(
State,
&self.state,
.unlocked,
new_state,
.Acquire,
.Monotonic,
) orelse return;
switch (state) {
.unlocked => {},
.locked => {},
.waiting => break,
}
var iter = std.math.min(32, spin + 1);
while (iter > 0) : (iter -= 1)
std.Thread.spinLoopHint();
}
new_state = .waiting;
while (true) {
switch (@atomicRmw(State, &self.state, .Xchg, new_state, .Acquire)) {
.unlocked => return,
else => {},
}
switch (std.Target.current.os.tag) {
.linux => {
switch (linux.getErrno(linux.futex_wait(
@ptrCast(*const i32, &self.state),
linux.FUTEX_PRIVATE_FLAG | linux.FUTEX_WAIT,
@enumToInt(new_state),
null,
))) {
0 => {},
std.os.EINTR => {},
std.os.EAGAIN => {},
else => unreachable,
}
},
else => std.Thread.spinLoopHint(),
}
}
}
pub fn release(self: *AtomicMutex) void {
switch (@atomicRmw(State, &self.state, .Xchg, .unlocked, .Release)) {
.unlocked => unreachable,
.locked => {},
.waiting => self.unlockSlow(),
}
}
fn unlockSlow(self: *AtomicMutex) void {
@setCold(true);
switch (std.Target.current.os.tag) {
.linux => {
switch (linux.getErrno(linux.futex_wake(
@ptrCast(*const i32, &self.state),
linux.FUTEX_PRIVATE_FLAG | linux.FUTEX_WAKE,
1,
))) {
0 => {},
std.os.EFAULT => {},
else => unreachable,
}
},
else => {},
}
}
};
pub const PthreadMutex = struct {
pthread_mutex: std.c.pthread_mutex_t = .{},
/// Try to acquire the mutex without blocking. Returns null if
/// the mutex is unavailable. Otherwise returns Held. Call
/// release on Held.
pub fn tryAcquire(self: *PthreadMutex) bool {
return std.c.pthread_mutex_trylock(&self.pthread_mutex) == 0;
}
/// Acquire the mutex. Will deadlock if the mutex is already
/// held by the calling thread.
pub fn acquire(self: *PthreadMutex) void {
switch (std.c.pthread_mutex_lock(&self.pthread_mutex)) {
0 => return,
std.c.EINVAL => unreachable,
std.c.EBUSY => unreachable,
std.c.EAGAIN => unreachable,
std.c.EDEADLK => unreachable,
std.c.EPERM => unreachable,
else => unreachable,
}
}
pub fn release(self: *PthreadMutex) void {
switch (std.c.pthread_mutex_unlock(&self.pthread_mutex)) {
0 => return,
std.c.EINVAL => unreachable,
std.c.EAGAIN => unreachable,
std.c.EPERM => unreachable,
else => unreachable,
}
}
};
/// This has the sematics as `Mutex`, however it does not actually do any
/// synchronization. Operations are safety-checked no-ops.
pub const Dummy = struct {
lock: @TypeOf(lock_init) = lock_init,
const lock_init = if (std.debug.runtime_safety) false else {};
/// Try to acquire the mutex without blocking. Returns null if
/// the mutex is unavailable. Otherwise returns Held. Call
/// release on Held.
pub fn tryAcquire(self: *Dummy) bool {
if (std.debug.runtime_safety) {
if (self.lock) return false;
self.lock = true;
}
return true;
}
/// Acquire the mutex. Will deadlock if the mutex is already
/// held by the calling thread.
pub fn acquire(self: *Dummy) void {
return self.tryAcquire() orelse @panic("deadlock detected");
}
pub fn release(self: *Dummy) void {
if (std.debug.runtime_safety) {
self.mutex.lock = false;
}
}
};
const WindowsMutex = struct {
srwlock: windows.SRWLOCK = windows.SRWLOCK_INIT,
pub fn tryAcquire(self: *WindowsMutex) bool {
return TryAcquireSRWLockExclusive(&self.srwlock) != system.FALSE;
}
pub fn acquire(self: *WindowsMutex) void {
AcquireSRWLockExclusive(&self.srwlock);
}
pub fn release(self: *WindowsMutex) void {
ReleaseSRWLockExclusive(&self.srwlock);
}
};
const TestContext = struct {
mutex: *Mutex,
data: i128,
const incr_count = 10000;
};
test "basic usage" {
var mutex = Mutex{};
var context = TestContext{
.mutex = &mutex,
.data = 0,
};
if (builtin.single_threaded) {
worker(&context);
testing.expect(context.data == TestContext.incr_count);
} else {
const thread_count = 10;
var threads: [thread_count]*std.Thread = undefined;
for (threads) |*t| {
t.* = try std.Thread.spawn(&context, worker);
}
for (threads) |t|
t.wait();
testing.expect(context.data == thread_count * TestContext.incr_count);
}
}
fn worker(ctx: *TestContext) void {
var i: usize = 0;
while (i != TestContext.incr_count) : (i += 1) {
const held = ctx.mutex.acquire();
defer held.release();
ctx.data += 1;
}
}

View File

@ -9,11 +9,11 @@
//! This API requires being initialized at runtime, and initialization //! This API requires being initialized at runtime, and initialization
//! can fail. Once initialized, the core operations cannot fail. //! can fail. Once initialized, the core operations cannot fail.
//! If you need an abstraction that cannot fail to be initialized, see //! If you need an abstraction that cannot fail to be initialized, see
//! `std.StaticResetEvent`. However if you can handle initialization failure, //! `std.Thread.StaticResetEvent`. However if you can handle initialization failure,
//! it is preferred to use `ResetEvent`. //! it is preferred to use `ResetEvent`.
const ResetEvent = @This(); const ResetEvent = @This();
const std = @import("std.zig"); const std = @import("../std.zig");
const builtin = std.builtin; const builtin = std.builtin;
const testing = std.testing; const testing = std.testing;
const assert = std.debug.assert; const assert = std.debug.assert;
@ -24,13 +24,13 @@ const time = std.time;
impl: Impl, impl: Impl,
pub const Impl = if (builtin.single_threaded) pub const Impl = if (builtin.single_threaded)
std.StaticResetEvent.DebugEvent std.Thread.StaticResetEvent.DebugEvent
else if (std.Target.current.isDarwin()) else if (std.Target.current.isDarwin())
DarwinEvent DarwinEvent
else if (std.Thread.use_pthreads) else if (std.Thread.use_pthreads)
PosixEvent PosixEvent
else else
std.StaticResetEvent.AtomicEvent; std.Thread.StaticResetEvent.AtomicEvent;
pub const InitError = error{SystemResources}; pub const InitError = error{SystemResources};

308
lib/std/Thread/RwLock.zig Normal file
View File

@ -0,0 +1,308 @@
// 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.
//! A lock that supports one writer or many readers.
//! This API is for kernel threads, not evented I/O.
//! This API requires being initialized at runtime, and initialization
//! can fail. Once initialized, the core operations cannot fail.
impl: Impl,
const RwLock = @This();
const std = @import("../std.zig");
const builtin = std.builtin;
const assert = std.debug.assert;
const Mutex = std.Thread.Mutex;
const Semaphore = std.Semaphore;
const CondVar = std.CondVar;
pub const Impl = if (builtin.single_threaded)
SingleThreadedRwLock
else if (std.Thread.use_pthreads)
PthreadRwLock
else
DefaultRwLock;
pub fn init(rwl: *RwLock) void {
return rwl.impl.init();
}
pub fn deinit(rwl: *RwLock) void {
return rwl.impl.deinit();
}
/// Attempts to obtain exclusive lock ownership.
/// Returns `true` if the lock is obtained, `false` otherwise.
pub fn tryLock(rwl: *RwLock) bool {
return rwl.impl.tryLock();
}
/// Blocks until exclusive lock ownership is acquired.
pub fn lock(rwl: *RwLock) void {
return rwl.impl.lock();
}
/// Releases a held exclusive lock.
/// Asserts the lock is held exclusively.
pub fn unlock(rwl: *RwLock) void {
return rwl.impl.unlock();
}
/// Attempts to obtain shared lock ownership.
/// Returns `true` if the lock is obtained, `false` otherwise.
pub fn tryLockShared(rwl: *RwLock) bool {
return rwl.impl.tryLockShared();
}
/// Blocks until shared lock ownership is acquired.
pub fn lockShared(rwl: *RwLock) void {
return rwl.impl.lockShared();
}
/// Releases a held shared lock.
pub fn unlockShared(rwl: *RwLock) void {
return rwl.impl.unlockShared();
}
/// Single-threaded applications use this for deadlock checks in
/// debug mode, and no-ops in release modes.
pub const SingleThreadedRwLock = struct {
state: enum { unlocked, locked_exclusive, locked_shared },
shared_count: usize,
pub fn init(rwl: *SingleThreadedRwLock) void {
rwl.* = .{
.state = .unlocked,
.shared_count = 0,
};
}
pub fn deinit(rwl: *SingleThreadedRwLock) void {
assert(rwl.state == .unlocked);
assert(rwl.shared_count == 0);
}
/// Attempts to obtain exclusive lock ownership.
/// Returns `true` if the lock is obtained, `false` otherwise.
pub fn tryLock(rwl: *SingleThreadedRwLock) bool {
switch (rwl.state) {
.unlocked => {
assert(rwl.shared_count == 0);
rwl.state = .locked_exclusive;
return true;
},
.locked_exclusive, .locked_shared => return false,
}
}
/// Blocks until exclusive lock ownership is acquired.
pub fn lock(rwl: *SingleThreadedRwLock) void {
assert(rwl.state == .unlocked); // deadlock detected
assert(rwl.shared_count == 0); // corrupted state detected
rwl.state = .locked_exclusive;
}
/// Releases a held exclusive lock.
/// Asserts the lock is held exclusively.
pub fn unlock(rwl: *SingleThreadedRwLock) void {
assert(rwl.state == .locked_exclusive);
assert(rwl.shared_count == 0); // corrupted state detected
rwl.state = .unlocked;
}
/// Attempts to obtain shared lock ownership.
/// Returns `true` if the lock is obtained, `false` otherwise.
pub fn tryLockShared(rwl: *SingleThreadedRwLock) bool {
switch (rwl.state) {
.unlocked => {
rwl.state = .locked_shared;
assert(rwl.shared_count == 0);
rwl.shared_count = 1;
return true;
},
.locked_exclusive, .locked_shared => return false,
}
}
/// Blocks until shared lock ownership is acquired.
pub fn lockShared(rwl: *SingleThreadedRwLock) void {
switch (rwl.state) {
.unlocked => {
rwl.state = .locked_shared;
assert(rwl.shared_count == 0);
rwl.shared_count = 1;
},
.locked_shared => {
rwl.shared_count += 1;
},
.locked_exclusive => unreachable, // deadlock detected
}
}
/// Releases a held shared lock.
pub fn unlockShared(rwl: *SingleThreadedRwLock) void {
switch (rwl.state) {
.unlocked => unreachable, // too many calls to `unlockShared`
.locked_exclusive => unreachable, // exclusively held lock
.locked_shared => {
rwl.shared_count -= 1;
if (rwl.shared_count == 0) {
rwl.state = .unlocked;
}
},
}
}
};
pub const PthreadRwLock = struct {
rwlock: pthread_rwlock_t,
pub fn init(rwl: *PthreadRwLock) void {
rwl.* = .{ .rwlock = .{} };
}
pub fn deinit(rwl: *PthreadRwLock) void {
const safe_rc = switch (std.builtin.os.tag) {
.dragonfly, .netbsd => std.os.EAGAIN,
else => 0,
};
const rc = std.c.pthread_rwlock_destroy(&rwl.rwlock);
assert(rc == 0 or rc == safe_rc);
rwl.* = undefined;
}
pub fn tryLock(rwl: *PthreadRwLock) bool {
return pthread_rwlock_trywrlock(&rwl.rwlock) == 0;
}
pub fn lock(rwl: *PthreadRwLock) void {
const rc = pthread_rwlock_wrlock(&rwl.rwlock);
assert(rc == 0);
}
pub fn unlock(rwl: *PthreadRwLock) void {
const rc = pthread_rwlock_unlock(&rwl.rwlock);
assert(rc == 0);
}
pub fn tryLockShared(rwl: *PthreadRwLock) bool {
return pthread_rwlock_tryrdlock(&rwl.rwlock) == 0;
}
pub fn lockShared(rwl: *PthreadRwLock) void {
const rc = pthread_rwlock_rdlock(&rwl.rwlock);
assert(rc == 0);
}
pub fn unlockShared(rwl: *PthreadRwLock) void {
const rc = pthread_rwlock_unlock(&rwl.rwlock);
assert(rc == 0);
}
};
pub const DefaultRwLock = struct {
state: usize,
mutex: Mutex,
semaphore: Semaphore,
const IS_WRITING: usize = 1;
const WRITER: usize = 1 << 1;
const READER: usize = 1 << (1 + std.meta.bitCount(Count));
const WRITER_MASK: usize = std.math.maxInt(Count) << @ctz(usize, WRITER);
const READER_MASK: usize = std.math.maxInt(Count) << @ctz(usize, READER);
const Count = std.meta.Int(.unsigned, @divFloor(std.meta.bitCount(usize) - 1, 2));
pub fn init(rwl: *DefaultRwLock) void {
rwl.* = .{
.state = 0,
.mutex = Mutex.init(),
.semaphore = Semaphore.init(0),
};
}
pub fn deinit(rwl: *DefaultRwLock) void {
rwl.semaphore.deinit();
rwl.mutex.deinit();
rwl.* = undefined;
}
pub fn tryLock(rwl: *DefaultRwLock) bool {
if (rwl.mutex.tryLock()) {
const state = @atomicLoad(usize, &rwl.state, .SeqCst);
if (state & READER_MASK == 0) {
_ = @atomicRmw(usize, &rwl.state, .Or, IS_WRITING, .SeqCst);
return true;
}
rwl.mutex.unlock();
}
return false;
}
pub fn lock(rwl: *DefaultRwLock) void {
_ = @atomicRmw(usize, &rwl.state, .Add, WRITER, .SeqCst);
rwl.mutex.lock();
const state = @atomicRmw(usize, &rwl.state, .Or, IS_WRITING, .SeqCst);
if (state & READER_MASK != 0)
rwl.semaphore.wait();
}
pub fn unlock(rwl: *DefaultRwLock) void {
_ = @atomicRmw(usize, &rwl.state, .And, ~IS_WRITING, .SeqCst);
rwl.mutex.unlock();
}
pub fn tryLockShared(rwl: *DefaultRwLock) bool {
const state = @atomicLoad(usize, &rwl.state, .SeqCst);
if (state & (IS_WRITING | WRITER_MASK) == 0) {
_ = @cmpxchgStrong(
usize,
&rwl.state,
state,
state + READER,
.SeqCst,
.SeqCst,
) orelse return true;
}
if (rwl.mutex.tryLock()) {
_ = @atomicRmw(usize, &rwl.state, .Add, READER, .SeqCst);
rwl.mutex.unlock();
return true;
}
return false;
}
pub fn lockShared(rwl: *DefaultRwLock) void {
var state = @atomicLoad(usize, &rwl.state, .SeqCst);
while (state & (IS_WRITING | WRITER_MASK) == 0) {
state = @cmpxchgWeak(
usize,
&rwl.state,
state,
state + READER,
.SeqCst,
.SeqCst,
) orelse return;
}
rwl.mutex.lock();
_ = @atomicRmw(usize, &rwl.state, .Add, READER, .SeqCst);
rwl.mutex.unlock();
}
pub fn unlockShared(rwl: *DefaultRwLock) void {
const state = @atomicRmw(usize, &rwl.state, .Sub, READER, .SeqCst);
if ((state & READER_MASK == READER) and (state & IS_WRITING != 0))
rwl.semaphore.post();
}
};

View File

@ -0,0 +1,39 @@
// 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.
//! A semaphore is an unsigned integer that blocks the kernel thread if
//! the number would become negative.
//! This API supports static initialization and does not require deinitialization.
mutex: Mutex = .{},
cond: Condition = .{},
//! It is OK to initialize this field to any value.
permits: usize = 0,
const RwLock = @This();
const std = @import("../std.zig");
const Mutex = std.Thread.Mutex;
const Condition = std.Thread.Condition;
pub fn wait(sem: *Semaphore) void {
const held = sem.mutex.acquire();
defer held.release();
while (sem.permits == 0)
sem.cond.wait(&sem.mutex);
sem.permits -= 1;
if (sem.permits > 0)
sem.cond.signal();
}
pub fn post(sem: *Semaphore) void {
const held = sem.mutex.acquire();
defer held.release();
sem.permits += 1;
sem.cond.signal();
}

View File

@ -10,10 +10,10 @@
//! and it requires no deinitialization. The downside is that it may not //! and it requires no deinitialization. The downside is that it may not
//! integrate as cleanly into other synchronization APIs, or, in a worst case, //! integrate as cleanly into other synchronization APIs, or, in a worst case,
//! may be forced to fall back on spin locking. As a rule of thumb, prefer //! may be forced to fall back on spin locking. As a rule of thumb, prefer
//! to use `std.ResetEvent` when possible, and use `StaticResetEvent` when //! to use `std.Thread.ResetEvent` when possible, and use `StaticResetEvent` when
//! the logic needs stronger API guarantees. //! the logic needs stronger API guarantees.
const std = @import("std.zig"); const std = @import("../std.zig");
const StaticResetEvent = @This(); const StaticResetEvent = @This();
const SpinLock = std.SpinLock; const SpinLock = std.SpinLock;
const assert = std.debug.assert; const assert = std.debug.assert;
@ -53,7 +53,7 @@ pub fn reset(ev: *StaticResetEvent) void {
return ev.impl.reset(); return ev.impl.reset();
} }
pub const TimedWaitResult = std.ResetEvent.TimedWaitResult; pub const TimedWaitResult = std.Thread.ResetEvent.TimedWaitResult;
/// Wait for the event to be set by blocking the current thread. /// Wait for the event to be set by blocking the current thread.
/// A timeout in nanoseconds can be provided as a hint for how /// A timeout in nanoseconds can be provided as a hint for how
@ -78,13 +78,13 @@ pub const DebugEvent = struct {
}; };
/// This function is provided so that this type can be re-used inside /// This function is provided so that this type can be re-used inside
/// `std.ResetEvent`. /// `std.Thread.ResetEvent`.
pub fn init(ev: *DebugEvent) void { pub fn init(ev: *DebugEvent) void {
ev.* = .{}; ev.* = .{};
} }
/// This function is provided so that this type can be re-used inside /// This function is provided so that this type can be re-used inside
/// `std.ResetEvent`. /// `std.Thread.ResetEvent`.
pub fn deinit(ev: *DebugEvent) void { pub fn deinit(ev: *DebugEvent) void {
ev.* = undefined; ev.* = undefined;
} }
@ -125,13 +125,13 @@ pub const AtomicEvent = struct {
const WAIT = 1 << 1; const WAIT = 1 << 1;
/// This function is provided so that this type can be re-used inside /// This function is provided so that this type can be re-used inside
/// `std.ResetEvent`. /// `std.Thread.ResetEvent`.
pub fn init(ev: *AtomicEvent) void { pub fn init(ev: *AtomicEvent) void {
ev.* = .{}; ev.* = .{};
} }
/// This function is provided so that this type can be re-used inside /// This function is provided so that this type can be re-used inside
/// `std.ResetEvent`. /// `std.Thread.ResetEvent`.
pub fn deinit(ev: *AtomicEvent) void { pub fn deinit(ev: *AtomicEvent) void {
ev.* = undefined; ev.* = undefined;
} }

View File

@ -16,7 +16,7 @@ pub fn Queue(comptime T: type) type {
return struct { return struct {
head: ?*Node, head: ?*Node,
tail: ?*Node, tail: ?*Node,
mutex: std.Mutex, mutex: std.Thread.Mutex,
pub const Self = @This(); pub const Self = @This();
pub const Node = std.TailQueue(T).Node; pub const Node = std.TailQueue(T).Node;
@ -27,7 +27,7 @@ pub fn Queue(comptime T: type) type {
return Self{ return Self{
.head = null, .head = null,
.tail = null, .tail = null,
.mutex = std.Mutex{}, .mutex = std.Thread.Mutex{},
}; };
} }

View File

@ -1,226 +0,0 @@
// 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.
const std = @import("std.zig");
const builtin = @import("builtin");
const testing = std.testing;
const assert = std.debug.assert;
const StaticResetEvent = std.StaticResetEvent;
/// Similar to `StaticResetEvent` but on `set()` it also (atomically) does `reset()`.
/// Unlike StaticResetEvent, `wait()` can only be called by one thread (MPSC-like).
pub const AutoResetEvent = struct {
/// 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 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 "std.AutoResetEvent" {
// 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, Context.sender);
const recv_thread = try std.Thread.spawn(&context, Context.receiver);
send_thread.wait();
recv_thread.wait();
}

View File

@ -338,6 +338,13 @@ pub extern "c" fn pthread_cond_signal(cond: *pthread_cond_t) c_int;
pub extern "c" fn pthread_cond_broadcast(cond: *pthread_cond_t) c_int; pub extern "c" fn pthread_cond_broadcast(cond: *pthread_cond_t) c_int;
pub extern "c" fn pthread_cond_destroy(cond: *pthread_cond_t) c_int; pub extern "c" fn pthread_cond_destroy(cond: *pthread_cond_t) c_int;
pub extern "c" fn pthread_rwlock_destroy(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub extern "c" fn pthread_rwlock_rdlock(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub extern "c" fn pthread_rwlock_wrlock(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub extern "c" fn pthread_rwlock_tryrdlock(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub extern "c" fn pthread_rwlock_trywrlock(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub extern "c" fn pthread_rwlock_unlock(rwl: *pthread_rwlock_t) callconv(.C) c_int;
pub const pthread_t = *opaque {}; pub const pthread_t = *opaque {};
pub const FILE = opaque {}; pub const FILE = opaque {};

View File

@ -177,6 +177,10 @@ pub const pthread_cond_t = extern struct {
__sig: c_long = 0x3CB0B1BB, __sig: c_long = 0x3CB0B1BB,
__opaque: [__PTHREAD_COND_SIZE__]u8 = [_]u8{0} ** __PTHREAD_COND_SIZE__, __opaque: [__PTHREAD_COND_SIZE__]u8 = [_]u8{0} ** __PTHREAD_COND_SIZE__,
}; };
pub const pthread_rwlock_t = extern struct {
__sig: c_long = 0x2DA8B3B4,
__opaque: [192]u8 = [_]u8{0} ** 192,
};
pub const sem_t = c_int; pub const sem_t = c_int;
const __PTHREAD_MUTEX_SIZE__ = if (@sizeOf(usize) == 8) 56 else 40; const __PTHREAD_MUTEX_SIZE__ = if (@sizeOf(usize) == 8) 56 else 40;
const __PTHREAD_COND_SIZE__ = if (@sizeOf(usize) == 8) 40 else 24; const __PTHREAD_COND_SIZE__ = if (@sizeOf(usize) == 8) 40 else 24;

View File

@ -9,5 +9,8 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T, size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T,
}; };
pub const pthread_rwlock_t = extern struct {
size: [32]u8 align(4) = [_]u8{0} ** 32,
};
const __SIZEOF_PTHREAD_COND_T = 48; const __SIZEOF_PTHREAD_COND_T = 48;
const __SIZEOF_PTHREAD_MUTEX_T = 28; const __SIZEOF_PTHREAD_MUTEX_T = 28;

View File

@ -43,6 +43,9 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
inner: ?*c_void = null, inner: ?*c_void = null,
}; };
pub const pthread_rwlock_t = extern struct {
ptr: ?*c_void = null,
};
pub const pthread_attr_t = extern struct { pub const pthread_attr_t = extern struct {
__size: [56]u8, __size: [56]u8,

View File

@ -9,5 +9,8 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T, size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T,
}; };
pub const pthread_rwlock_t = extern struct {
size: [56]u8 align(@alignOf(usize)) = [_]u8{0} ** 56,
};
const __SIZEOF_PTHREAD_COND_T = 48; const __SIZEOF_PTHREAD_COND_T = 48;
const __SIZEOF_PTHREAD_MUTEX_T = 40; const __SIZEOF_PTHREAD_MUTEX_T = 40;

View File

@ -17,3 +17,12 @@ pub const pthread_cond_t = extern struct {
waiter_count: i32 = 0, waiter_count: i32 = 0,
lock: i32 = 0, lock: i32 = 0,
}; };
pub const pthread_rwlock_t = extern struct {
flags: u32 = 0,
owner: i32 = -1,
lock_sem: i32 = 0,
lock_count: i32 = 0,
reader_count: i32 = 0,
writer_count: i32 = 0,
waiters: [2]?*c_void = [_]?*c_void{ null, null },
};

View File

@ -9,3 +9,6 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
inner: usize = ~@as(usize, 0), inner: usize = ~@as(usize, 0),
}; };
pub const pthread_rwlock_t = extern struct {
ptr: usize = std.math.maxInt(usize),
};

View File

@ -123,6 +123,32 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T, size: [__SIZEOF_PTHREAD_COND_T]u8 align(@alignOf(usize)) = [_]u8{0} ** __SIZEOF_PTHREAD_COND_T,
}; };
pub const pthread_rwlock_t = switch (std.builtin.abi) {
.android => switch (@sizeOf(usize)) {
4 => extern struct {
lock: std.c.pthread_mutex_t = std.c.PTHREAD_MUTEX_INITIALIZER,
cond: std.c.pthread_cond_t = std.c.PTHREAD_COND_INITIALIZER,
numLocks: c_int = 0,
writerThreadId: c_int = 0,
pendingReaders: c_int = 0,
pendingWriters: c_int = 0,
attr: i32 = 0,
__reserved: [12]u8 = [_]u8{0} ** 2,
},
8 => extern struct {
numLocks: c_int = 0,
writerThreadId: c_int = 0,
pendingReaders: c_int = 0,
pendingWriters: c_int = 0,
attr: i32 = 0,
__reserved: [36]u8 = [_]u8{0} ** 36,
},
else => unreachable,
},
else => extern struct {
size: [56]u8 align(@alignOf(usize)) = [_]u8{0} ** 56,
},
};
pub const sem_t = extern struct { pub const sem_t = extern struct {
__size: [__SIZEOF_SEM_T]u8 align(@alignOf(usize)), __size: [__SIZEOF_SEM_T]u8 align(@alignOf(usize)),
}; };

View File

@ -54,6 +54,22 @@ pub const pthread_cond_t = extern struct {
ptc_private: ?*c_void = null, ptc_private: ?*c_void = null,
}; };
pub const pthread_rwlock_t = extern struct {
ptr_magic: c_uint = 0x99990009,
ptr_interlock: switch (std.builtin.arch) {
.aarch64, .sparc, .x86_64, .i386 => u8,
.arm, .powerpc => c_int,
else => unreachable,
} = 0,
ptr_rblocked_first: ?*u8 = null,
ptr_rblocked_last: ?*u8 = null,
ptr_wblocked_first: ?*u8 = null,
ptr_wblocked_last: ?*u8 = null,
ptr_nreaders: c_uint = 0,
ptr_owner: std.c.pthread_t = null,
ptr_private: ?*c_void = null,
};
const pthread_spin_t = switch (builtin.arch) { const pthread_spin_t = switch (builtin.arch) {
.aarch64, .aarch64_be, .aarch64_32 => u8, .aarch64, .aarch64_be, .aarch64_32 => u8,
.mips, .mipsel, .mips64, .mips64el => u32, .mips, .mipsel, .mips64, .mips64el => u32,

View File

@ -27,6 +27,9 @@ pub const pthread_mutex_t = extern struct {
pub const pthread_cond_t = extern struct { pub const pthread_cond_t = extern struct {
inner: ?*c_void = null, inner: ?*c_void = null,
}; };
pub const pthread_rwlock_t = extern struct {
ptr: ?*c_void = null,
};
pub const pthread_spinlock_t = extern struct { pub const pthread_spinlock_t = extern struct {
inner: ?*c_void = null, inner: ?*c_void = null,
}; };

View File

@ -50,7 +50,7 @@ pub const LineInfo = struct {
} }
}; };
var stderr_mutex = std.Mutex{}; var stderr_mutex = std.Thread.Mutex{};
/// Deprecated. Use `std.log` functions for logging or `std.debug.print` for /// Deprecated. Use `std.log` functions for logging or `std.debug.print` for
/// "printf debugging". /// "printf debugging".
@ -65,7 +65,7 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
nosuspend stderr.print(fmt, args) catch return; nosuspend stderr.print(fmt, args) catch return;
} }
pub fn getStderrMutex() *std.Mutex { pub fn getStderrMutex() *std.Thread.Mutex {
return &stderr_mutex; return &stderr_mutex;
} }
@ -235,7 +235,7 @@ pub fn panic(comptime format: []const u8, args: anytype) noreturn {
var panicking: u8 = 0; var panicking: u8 = 0;
// Locked to avoid interleaving panic messages from multiple threads. // Locked to avoid interleaving panic messages from multiple threads.
var panic_mutex = std.Mutex{}; var panic_mutex = std.Thread.Mutex{};
/// Counts how many times the panic handler is invoked by this thread. /// Counts how many times the panic handler is invoked by this thread.
/// This is used to catch and handle panics triggered by the panic handler. /// This is used to catch and handle panics triggered by the panic handler.
@ -280,7 +280,7 @@ pub fn panicExtra(trace: ?*const builtin.StackTrace, first_trace_addr: ?usize, c
// and call abort() // and call abort()
// Sleep forever without hammering the CPU // Sleep forever without hammering the CPU
var event: std.StaticResetEvent = .{}; var event: std.Thread.StaticResetEvent = .{};
event.wait(); event.wait();
unreachable; unreachable;
} }

View File

@ -16,7 +16,7 @@ const Loop = std.event.Loop;
/// Allows only one actor to hold the lock. /// Allows only one actor to hold the lock.
/// TODO: make this API also work in blocking I/O mode. /// TODO: make this API also work in blocking I/O mode.
pub const Lock = struct { pub const Lock = struct {
mutex: std.Mutex = std.Mutex{}, mutex: std.Thread.Mutex = std.Thread.Mutex{},
head: usize = UNLOCKED, head: usize = UNLOCKED,
const UNLOCKED = 0; const UNLOCKED = 0;

View File

@ -29,7 +29,7 @@ pub const Loop = struct {
fs_thread: *Thread, fs_thread: *Thread,
fs_queue: std.atomic.Queue(Request), fs_queue: std.atomic.Queue(Request),
fs_end_request: Request.Node, fs_end_request: Request.Node,
fs_thread_wakeup: std.ResetEvent, fs_thread_wakeup: std.Thread.ResetEvent,
/// For resources that have the same lifetime as the `Loop`. /// For resources that have the same lifetime as the `Loop`.
/// This is only used by `Loop` for the thread pool and associated resources. /// This is only used by `Loop` for the thread pool and associated resources.
@ -785,7 +785,7 @@ pub const Loop = struct {
timer: std.time.Timer, timer: std.time.Timer,
waiters: Waiters, waiters: Waiters,
thread: *std.Thread, thread: *std.Thread,
event: std.AutoResetEvent, event: std.Thread.AutoResetEvent,
is_running: bool, is_running: bool,
/// Initialize the delay queue by spawning the timer thread /// Initialize the delay queue by spawning the timer thread
@ -796,7 +796,7 @@ pub const Loop = struct {
.waiters = DelayQueue.Waiters{ .waiters = DelayQueue.Waiters{
.entries = std.atomic.Queue(anyframe).init(), .entries = std.atomic.Queue(anyframe).init(),
}, },
.event = std.AutoResetEvent{}, .event = std.Thread.AutoResetEvent{},
.is_running = true, .is_running = true,
// Must be last so that it can read the other state, such as `is_running`. // Must be last so that it can read the other state, such as `is_running`.
.thread = try std.Thread.spawn(self, DelayQueue.run), .thread = try std.Thread.spawn(self, DelayQueue.run),

View File

@ -30,7 +30,7 @@ pub fn WaitGroupGeneric(comptime counter_size: u16) type {
return struct { return struct {
counter: CounterType = 0, counter: CounterType = 0,
max_counter: CounterType = std.math.maxInt(CounterType), max_counter: CounterType = std.math.maxInt(CounterType),
mutex: std.Mutex = .{}, mutex: std.Thread.Mutex = .{},
waiters: ?*Waiter = null, waiters: ?*Waiter = null,
const Waiter = struct { const Waiter = struct {
next: ?*Waiter, next: ?*Waiter,

View File

@ -750,7 +750,7 @@ test "open file with exclusive lock twice, make sure it waits" {
errdefer file.close(); errdefer file.close();
const S = struct { const S = struct {
const C = struct { dir: *fs.Dir, evt: *std.ResetEvent }; const C = struct { dir: *fs.Dir, evt: *std.Thread.ResetEvent };
fn checkFn(ctx: C) !void { fn checkFn(ctx: C) !void {
const file1 = try ctx.dir.createFile(filename, .{ .lock = .Exclusive }); const file1 = try ctx.dir.createFile(filename, .{ .lock = .Exclusive });
defer file1.close(); defer file1.close();
@ -758,7 +758,7 @@ test "open file with exclusive lock twice, make sure it waits" {
} }
}; };
var evt: std.ResetEvent = undefined; var evt: std.Thread.ResetEvent = undefined;
try evt.init(); try evt.init();
defer evt.deinit(); defer evt.deinit();

View File

@ -149,12 +149,12 @@ pub const Config = struct {
thread_safe: bool = !std.builtin.single_threaded, thread_safe: bool = !std.builtin.single_threaded,
/// What type of mutex you'd like to use, for thread safety. /// What type of mutex you'd like to use, for thread safety.
/// when specfied, the mutex type must have the same shape as `std.Mutex` and /// when specfied, the mutex type must have the same shape as `std.Thread.Mutex` and
/// `std.mutex.Dummy`, and have no required fields. Specifying this field causes /// `std.mutex.Dummy`, and have no required fields. Specifying this field causes
/// the `thread_safe` field to be ignored. /// the `thread_safe` field to be ignored.
/// ///
/// when null (default): /// when null (default):
/// * the mutex type defaults to `std.Mutex` when thread_safe is enabled. /// * the mutex type defaults to `std.Thread.Mutex` when thread_safe is enabled.
/// * the mutex type defaults to `std.mutex.Dummy` otherwise. /// * the mutex type defaults to `std.mutex.Dummy` otherwise.
MutexType: ?type = null, MutexType: ?type = null,
@ -187,7 +187,7 @@ pub fn GeneralPurposeAllocator(comptime config: Config) type {
const mutex_init = if (config.MutexType) |T| const mutex_init = if (config.MutexType) |T|
T{} T{}
else if (config.thread_safe) else if (config.thread_safe)
std.Mutex{} std.Thread.Mutex{}
else else
std.mutex.Dummy{}; std.mutex.Dummy{};
@ -869,9 +869,9 @@ test "realloc large object to small object" {
} }
test "overrideable mutexes" { test "overrideable mutexes" {
var gpa = GeneralPurposeAllocator(.{ .MutexType = std.Mutex }){ var gpa = GeneralPurposeAllocator(.{ .MutexType = std.Thread.Mutex }){
.backing_allocator = std.testing.allocator, .backing_allocator = std.testing.allocator,
.mutex = std.Mutex{}, .mutex = std.Thread.Mutex{},
}; };
defer std.testing.expect(!gpa.deinit()); defer std.testing.expect(!gpa.deinit());
const allocator = &gpa.allocator; const allocator = &gpa.allocator;

View File

@ -1,379 +0,0 @@
// 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.
const std = @import("std.zig");
const builtin = @import("builtin");
const os = std.os;
const assert = std.debug.assert;
const windows = os.windows;
const testing = std.testing;
const SpinLock = std.SpinLock;
const StaticResetEvent = std.StaticResetEvent;
/// Lock may be held only once. If the same thread tries to acquire
/// the same mutex twice, it deadlocks. This type supports static
/// initialization and is at most `@sizeOf(usize)` in size. When an
/// application is built in single threaded release mode, all the
/// functions are no-ops. In single threaded debug mode, there is
/// deadlock detection.
///
/// Example usage:
/// var m = Mutex{};
///
/// const lock = m.acquire();
/// defer lock.release();
/// ... critical code
///
/// Non-blocking:
/// if (m.tryAcquire) |lock| {
/// defer lock.release();
/// // ... critical section
/// } else {
/// // ... lock not acquired
/// }
pub const Mutex = if (builtin.single_threaded)
Dummy
else if (builtin.os.tag == .windows)
WindowsMutex
else if (std.Thread.use_pthreads)
PthreadMutex
else if (builtin.link_libc or builtin.os.tag == .linux)
// stack-based version of https://github.com/Amanieu/parking_lot/blob/master/core/src/word_lock.rs
struct {
state: usize = 0,
/// number of times to spin trying to acquire the lock.
/// https://webkit.org/blog/6161/locking-in-webkit/
const SPIN_COUNT = 40;
const MUTEX_LOCK: usize = 1 << 0;
const QUEUE_LOCK: usize = 1 << 1;
const QUEUE_MASK: usize = ~(MUTEX_LOCK | QUEUE_LOCK);
const Node = struct {
next: ?*Node,
event: StaticResetEvent,
};
pub fn tryAcquire(self: *Mutex) ?Held {
if (@cmpxchgWeak(usize, &self.state, 0, MUTEX_LOCK, .Acquire, .Monotonic) != null)
return null;
return Held{ .mutex = self };
}
pub fn acquire(self: *Mutex) Held {
return self.tryAcquire() orelse {
self.acquireSlow();
return Held{ .mutex = self };
};
}
fn acquireSlow(self: *Mutex) void {
// inlining the fast path and hiding *Slow()
// calls behind a @setCold(true) appears to
// improve performance in release builds.
@setCold(true);
while (true) {
// try and spin for a bit to acquire the mutex if theres currently no queue
var spin_count: u32 = SPIN_COUNT;
var state = @atomicLoad(usize, &self.state, .Monotonic);
while (spin_count != 0) : (spin_count -= 1) {
if (state & MUTEX_LOCK == 0) {
_ = @cmpxchgWeak(usize, &self.state, state, state | MUTEX_LOCK, .Acquire, .Monotonic) orelse return;
} else if (state & QUEUE_MASK == 0) {
break;
}
SpinLock.yield();
state = @atomicLoad(usize, &self.state, .Monotonic);
}
// create the StaticResetEvent node on the stack
// (faster than threadlocal on platforms like OSX)
var node: Node = .{
.next = undefined,
.event = .{},
};
// we've spun too long, try and add our node to the LIFO queue.
// if the mutex becomes available in the process, try and grab it instead.
while (true) {
if (state & MUTEX_LOCK == 0) {
_ = @cmpxchgWeak(usize, &self.state, state, state | MUTEX_LOCK, .Acquire, .Monotonic) orelse return;
} else {
node.next = @intToPtr(?*Node, state & QUEUE_MASK);
const new_state = @ptrToInt(&node) | (state & ~QUEUE_MASK);
_ = @cmpxchgWeak(usize, &self.state, state, new_state, .Release, .Monotonic) orelse {
node.event.wait();
break;
};
}
SpinLock.yield();
state = @atomicLoad(usize, &self.state, .Monotonic);
}
}
}
/// Returned when the lock is acquired. Call release to
/// release.
pub const Held = struct {
mutex: *Mutex,
/// Release the held lock.
pub fn release(self: Held) void {
// first, remove the lock bit so another possibly parallel acquire() can succeed.
// use .Sub since it can be usually compiled down more efficiency
// (`lock sub` on x86) vs .And ~MUTEX_LOCK (`lock cmpxchg` loop on x86)
const state = @atomicRmw(usize, &self.mutex.state, .Sub, MUTEX_LOCK, .Release);
// if the LIFO queue isnt locked and it has a node, try and wake up the node.
if ((state & QUEUE_LOCK) == 0 and (state & QUEUE_MASK) != 0)
self.mutex.releaseSlow();
}
};
fn releaseSlow(self: *Mutex) void {
@setCold(true);
// try and lock the LFIO queue to pop a node off,
// stopping altogether if its already locked or the queue is empty
var state = @atomicLoad(usize, &self.state, .Monotonic);
while (true) : (SpinLock.loopHint(1)) {
if (state & QUEUE_LOCK != 0 or state & QUEUE_MASK == 0)
return;
state = @cmpxchgWeak(usize, &self.state, state, state | QUEUE_LOCK, .Acquire, .Monotonic) orelse break;
}
// acquired the QUEUE_LOCK, try and pop a node to wake it.
// if the mutex is locked, then unset QUEUE_LOCK and let
// the thread who holds the mutex do the wake-up on unlock()
while (true) : (SpinLock.loopHint(1)) {
if ((state & MUTEX_LOCK) != 0) {
state = @cmpxchgWeak(usize, &self.state, state, state & ~QUEUE_LOCK, .Release, .Acquire) orelse return;
} else {
const node = @intToPtr(*Node, state & QUEUE_MASK);
const new_state = @ptrToInt(node.next);
state = @cmpxchgWeak(usize, &self.state, state, new_state, .Release, .Acquire) orelse {
node.event.set();
return;
};
}
}
}
}
// for platforms without a known OS blocking
// primitive, default to SpinLock for correctness
else
SpinLock;
pub const PthreadMutex = struct {
pthread_mutex: std.c.pthread_mutex_t = init,
pub const Held = struct {
mutex: *PthreadMutex,
pub fn release(self: Held) void {
switch (std.c.pthread_mutex_unlock(&self.mutex.pthread_mutex)) {
0 => return,
std.c.EINVAL => unreachable,
std.c.EAGAIN => unreachable,
std.c.EPERM => unreachable,
else => unreachable,
}
}
};
/// Create a new mutex in unlocked state.
pub const init = std.c.PTHREAD_MUTEX_INITIALIZER;
/// Try to acquire the mutex without blocking. Returns null if
/// the mutex is unavailable. Otherwise returns Held. Call
/// release on Held.
pub fn tryAcquire(self: *PthreadMutex) ?Held {
if (std.c.pthread_mutex_trylock(&self.pthread_mutex) == 0) {
return Held{ .mutex = self };
} else {
return null;
}
}
/// Acquire the mutex. Will deadlock if the mutex is already
/// held by the calling thread.
pub fn acquire(self: *PthreadMutex) Held {
switch (std.c.pthread_mutex_lock(&self.pthread_mutex)) {
0 => return Held{ .mutex = self },
std.c.EINVAL => unreachable,
std.c.EBUSY => unreachable,
std.c.EAGAIN => unreachable,
std.c.EDEADLK => unreachable,
std.c.EPERM => unreachable,
else => unreachable,
}
}
};
/// This has the sematics as `Mutex`, however it does not actually do any
/// synchronization. Operations are safety-checked no-ops.
pub const Dummy = struct {
lock: @TypeOf(lock_init) = lock_init,
const lock_init = if (std.debug.runtime_safety) false else {};
pub const Held = struct {
mutex: *Dummy,
pub fn release(self: Held) void {
if (std.debug.runtime_safety) {
self.mutex.lock = false;
}
}
};
/// Create a new mutex in unlocked state.
pub const init = Dummy{};
/// Try to acquire the mutex without blocking. Returns null if
/// the mutex is unavailable. Otherwise returns Held. Call
/// release on Held.
pub fn tryAcquire(self: *Dummy) ?Held {
if (std.debug.runtime_safety) {
if (self.lock) return null;
self.lock = true;
}
return Held{ .mutex = self };
}
/// Acquire the mutex. Will deadlock if the mutex is already
/// held by the calling thread.
pub fn acquire(self: *Dummy) Held {
return self.tryAcquire() orelse @panic("deadlock detected");
}
};
// https://locklessinc.com/articles/keyed_events/
const WindowsMutex = struct {
state: State = State{ .waiters = 0 },
const State = extern union {
locked: u8,
waiters: u32,
};
const WAKE = 1 << 8;
const WAIT = 1 << 9;
pub fn tryAcquire(self: *WindowsMutex) ?Held {
if (@atomicRmw(u8, &self.state.locked, .Xchg, 1, .Acquire) != 0)
return null;
return Held{ .mutex = self };
}
pub fn acquire(self: *WindowsMutex) Held {
return self.tryAcquire() orelse self.acquireSlow();
}
fn acquireSpinning(self: *WindowsMutex) Held {
@setCold(true);
while (true) : (SpinLock.yield()) {
return self.tryAcquire() orelse continue;
}
}
fn acquireSlow(self: *WindowsMutex) Held {
// try to use NT keyed events for blocking, falling back to spinlock if unavailable
@setCold(true);
const handle = StaticResetEvent.Impl.Futex.getEventHandle() orelse return self.acquireSpinning();
const key = @ptrCast(*const c_void, &self.state.waiters);
while (true) : (SpinLock.loopHint(1)) {
const waiters = @atomicLoad(u32, &self.state.waiters, .Monotonic);
// try and take lock if unlocked
if ((waiters & 1) == 0) {
if (@atomicRmw(u8, &self.state.locked, .Xchg, 1, .Acquire) == 0) {
return Held{ .mutex = self };
}
// otherwise, try and update the waiting count.
// then unset the WAKE bit so that another unlocker can wake up a thread.
} else if (@cmpxchgWeak(u32, &self.state.waiters, waiters, (waiters + WAIT) | 1, .Monotonic, .Monotonic) == null) {
const rc = windows.ntdll.NtWaitForKeyedEvent(handle, key, windows.FALSE, null);
assert(rc == .SUCCESS);
_ = @atomicRmw(u32, &self.state.waiters, .Sub, WAKE, .Monotonic);
}
}
}
pub const Held = struct {
mutex: *WindowsMutex,
pub fn release(self: Held) void {
// unlock without a rmw/cmpxchg instruction
@atomicStore(u8, @ptrCast(*u8, &self.mutex.state.locked), 0, .Release);
const handle = StaticResetEvent.Impl.Futex.getEventHandle() orelse return;
const key = @ptrCast(*const c_void, &self.mutex.state.waiters);
while (true) : (SpinLock.loopHint(1)) {
const waiters = @atomicLoad(u32, &self.mutex.state.waiters, .Monotonic);
// no one is waiting
if (waiters < WAIT) return;
// someone grabbed the lock and will do the wake instead
if (waiters & 1 != 0) return;
// someone else is currently waking up
if (waiters & WAKE != 0) return;
// try to decrease the waiter count & set the WAKE bit meaning a thread is waking up
if (@cmpxchgWeak(u32, &self.mutex.state.waiters, waiters, waiters - WAIT + WAKE, .Release, .Monotonic) == null) {
const rc = windows.ntdll.NtReleaseKeyedEvent(handle, key, windows.FALSE, null);
assert(rc == .SUCCESS);
return;
}
}
}
};
};
const TestContext = struct {
mutex: *Mutex,
data: i128,
const incr_count = 10000;
};
test "std.Mutex" {
var mutex = Mutex{};
var context = TestContext{
.mutex = &mutex,
.data = 0,
};
if (builtin.single_threaded) {
worker(&context);
testing.expect(context.data == TestContext.incr_count);
} else {
const thread_count = 10;
var threads: [thread_count]*std.Thread = undefined;
for (threads) |*t| {
t.* = try std.Thread.spawn(&context, worker);
}
for (threads) |t|
t.wait();
testing.expect(context.data == thread_count * TestContext.incr_count);
}
}
fn worker(ctx: *TestContext) void {
var i: usize = 0;
while (i != TestContext.incr_count) : (i += 1) {
const held = ctx.mutex.acquire();
defer held.release();
ctx.data += 1;
}
}

View File

@ -15,7 +15,7 @@ pub fn once(comptime f: fn () void) Once(f) {
pub fn Once(comptime f: fn () void) type { pub fn Once(comptime f: fn () void) type {
return struct { return struct {
done: bool = false, done: bool = false,
mutex: std.Mutex = std.Mutex{}, mutex: std.Thread.Mutex = std.Thread.Mutex{},
/// Call the function `f`. /// Call the function `f`.
/// If `call` is invoked multiple times `f` will be executed only the /// If `call` is invoked multiple times `f` will be executed only the

View File

@ -1635,3 +1635,8 @@ pub const OBJECT_NAME_INFORMATION = extern struct {
Name: UNICODE_STRING, Name: UNICODE_STRING,
}; };
pub const POBJECT_NAME_INFORMATION = *OBJECT_NAME_INFORMATION; pub const POBJECT_NAME_INFORMATION = *OBJECT_NAME_INFORMATION;
pub const SRWLOCK = usize;
pub const SRWLOCK_INIT: SRWLOCK = 0;
pub const CONDITION_VARIABLE = usize;
pub const CONDITION_VARIABLE_INIT: CONDITION_VARIABLE = 0;

View File

@ -289,3 +289,16 @@ pub extern "kernel32" fn K32QueryWorkingSet(hProcess: HANDLE, pv: PVOID, cb: DWO
pub extern "kernel32" fn K32QueryWorkingSetEx(hProcess: HANDLE, pv: PVOID, cb: DWORD) callconv(WINAPI) BOOL; pub extern "kernel32" fn K32QueryWorkingSetEx(hProcess: HANDLE, pv: PVOID, cb: DWORD) callconv(WINAPI) BOOL;
pub extern "kernel32" fn FlushFileBuffers(hFile: HANDLE) callconv(WINAPI) BOOL; pub extern "kernel32" fn FlushFileBuffers(hFile: HANDLE) callconv(WINAPI) BOOL;
pub extern "kernel32" fn WakeAllConditionVariable(c: *CONDITION_VARIABLE) callconv(WINAPI) void;
pub extern "kernel32" fn WakeConditionVariable(c: *CONDITION_VARIABLE) callconv(WINAPI) void;
pub extern "kernel32" fn SleepConditionVariableSRW(
c: *CONDITION_VARIABLE,
s: *SRWLOCK,
t: DWORD,
f: ULONG,
) callconv(WINAPI) BOOL;
pub extern "kernel32" fn TryAcquireSRWLockExclusive(s: *SRWLOCK) callconv(WINAPI) BOOL;
pub extern "kernel32" fn AcquireSRWLockExclusive(s: *SRWLOCK) callconv(WINAPI) void;
pub extern "kernel32" fn ReleaseSRWLockExclusive(s: *SRWLOCK) callconv(WINAPI) void;

View File

@ -13,7 +13,6 @@ pub const AutoArrayHashMap = array_hash_map.AutoArrayHashMap;
pub const AutoArrayHashMapUnmanaged = array_hash_map.AutoArrayHashMapUnmanaged; pub const AutoArrayHashMapUnmanaged = array_hash_map.AutoArrayHashMapUnmanaged;
pub const AutoHashMap = hash_map.AutoHashMap; pub const AutoHashMap = hash_map.AutoHashMap;
pub const AutoHashMapUnmanaged = hash_map.AutoHashMapUnmanaged; pub const AutoHashMapUnmanaged = hash_map.AutoHashMapUnmanaged;
pub const AutoResetEvent = @import("auto_reset_event.zig").AutoResetEvent;
pub const BufMap = @import("buf_map.zig").BufMap; pub const BufMap = @import("buf_map.zig").BufMap;
pub const BufSet = @import("buf_set.zig").BufSet; pub const BufSet = @import("buf_set.zig").BufSet;
pub const ChildProcess = @import("child_process.zig").ChildProcess; pub const ChildProcess = @import("child_process.zig").ChildProcess;
@ -21,26 +20,21 @@ pub const ComptimeStringMap = @import("comptime_string_map.zig").ComptimeStringM
pub const DynLib = @import("dynamic_library.zig").DynLib; pub const DynLib = @import("dynamic_library.zig").DynLib;
pub const HashMap = hash_map.HashMap; pub const HashMap = hash_map.HashMap;
pub const HashMapUnmanaged = hash_map.HashMapUnmanaged; pub const HashMapUnmanaged = hash_map.HashMapUnmanaged;
pub const mutex = @import("mutex.zig");
pub const Mutex = mutex.Mutex;
pub const PackedIntArray = @import("packed_int_array.zig").PackedIntArray; pub const PackedIntArray = @import("packed_int_array.zig").PackedIntArray;
pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayEndian; pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayEndian;
pub const PackedIntSlice = @import("packed_int_array.zig").PackedIntSlice; pub const PackedIntSlice = @import("packed_int_array.zig").PackedIntSlice;
pub const PackedIntSliceEndian = @import("packed_int_array.zig").PackedIntSliceEndian; pub const PackedIntSliceEndian = @import("packed_int_array.zig").PackedIntSliceEndian;
pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue; pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue;
pub const Progress = @import("Progress.zig"); pub const Progress = @import("Progress.zig");
pub const ResetEvent = @import("ResetEvent.zig");
pub const SemanticVersion = @import("SemanticVersion.zig"); pub const SemanticVersion = @import("SemanticVersion.zig");
pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList; pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList;
pub const SpinLock = @import("SpinLock.zig");
pub const StaticResetEvent = @import("StaticResetEvent.zig");
pub const StringHashMap = hash_map.StringHashMap; pub const StringHashMap = hash_map.StringHashMap;
pub const StringHashMapUnmanaged = hash_map.StringHashMapUnmanaged; pub const StringHashMapUnmanaged = hash_map.StringHashMapUnmanaged;
pub const StringArrayHashMap = array_hash_map.StringArrayHashMap; pub const StringArrayHashMap = array_hash_map.StringArrayHashMap;
pub const StringArrayHashMapUnmanaged = array_hash_map.StringArrayHashMapUnmanaged; pub const StringArrayHashMapUnmanaged = array_hash_map.StringArrayHashMapUnmanaged;
pub const TailQueue = @import("linked_list.zig").TailQueue; pub const TailQueue = @import("linked_list.zig").TailQueue;
pub const Target = @import("target.zig").Target; pub const Target = @import("target.zig").Target;
pub const Thread = @import("thread.zig").Thread; pub const Thread = @import("Thread.zig");
pub const array_hash_map = @import("array_hash_map.zig"); pub const array_hash_map = @import("array_hash_map.zig");
pub const atomic = @import("atomic.zig"); pub const atomic = @import("atomic.zig");
@ -98,12 +92,7 @@ test "" {
// server is hitting OOM. TODO revert this after stage2 arrives. // server is hitting OOM. TODO revert this after stage2 arrives.
_ = ChildProcess; _ = ChildProcess;
_ = DynLib; _ = DynLib;
_ = mutex;
_ = Mutex;
_ = Progress; _ = Progress;
_ = ResetEvent;
_ = SpinLock;
_ = StaticResetEvent;
_ = Target; _ = Target;
_ = Thread; _ = Thread;

View File

@ -1,526 +0,0 @@
// 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.
const std = @import("std.zig");
const builtin = std.builtin;
const os = std.os;
const mem = std.mem;
const windows = std.os.windows;
const c = std.c;
const assert = std.debug.assert;
const bad_startfn_ret = "expected return type of startFn to be 'u8', 'noreturn', 'void', or '!void'";
pub const Thread = struct {
data: Data,
pub const use_pthreads = std.Target.current.os.tag != .windows and builtin.link_libc;
/// Represents a kernel thread handle.
/// May be an integer or a pointer depending on the platform.
/// On Linux and POSIX, this is the same as Id.
pub const Handle = if (use_pthreads)
c.pthread_t
else switch (std.Target.current.os.tag) {
.linux => i32,
.windows => windows.HANDLE,
else => void,
};
/// Represents a unique ID per thread.
/// May be an integer or pointer depending on the platform.
/// On Linux and POSIX, this is the same as Handle.
pub const Id = switch (std.Target.current.os.tag) {
.windows => windows.DWORD,
else => Handle,
};
pub const Data = if (use_pthreads)
struct {
handle: Thread.Handle,
memory: []u8,
}
else switch (std.Target.current.os.tag) {
.linux => struct {
handle: Thread.Handle,
memory: []align(mem.page_size) u8,
},
.windows => struct {
handle: Thread.Handle,
alloc_start: *c_void,
heap_handle: windows.HANDLE,
},
else => struct {},
};
/// Returns the ID of the calling thread.
/// Makes a syscall every time the function is called.
/// On Linux and POSIX, this Id is the same as a Handle.
pub fn getCurrentId() Id {
if (use_pthreads) {
return c.pthread_self();
} else
return switch (std.Target.current.os.tag) {
.linux => os.linux.gettid(),
.windows => windows.kernel32.GetCurrentThreadId(),
else => @compileError("Unsupported OS"),
};
}
/// Returns the handle of this thread.
/// On Linux and POSIX, this is the same as Id.
/// On Linux, it is possible that the thread spawned with `spawn`
/// finishes executing entirely before the clone syscall completes. In this
/// case, this function will return 0 rather than the no-longer-existing thread's
/// pid.
pub fn handle(self: Thread) Handle {
return self.data.handle;
}
pub fn wait(self: *Thread) void {
if (use_pthreads) {
const err = c.pthread_join(self.data.handle, null);
switch (err) {
0 => {},
os.EINVAL => unreachable,
os.ESRCH => unreachable,
os.EDEADLK => unreachable,
else => unreachable,
}
std.heap.c_allocator.free(self.data.memory);
std.heap.c_allocator.destroy(self);
} else switch (std.Target.current.os.tag) {
.linux => {
while (true) {
const pid_value = @atomicLoad(i32, &self.data.handle, .SeqCst);
if (pid_value == 0) break;
const rc = os.linux.futex_wait(&self.data.handle, os.linux.FUTEX_WAIT, pid_value, null);
switch (os.linux.getErrno(rc)) {
0 => continue,
os.EINTR => continue,
os.EAGAIN => continue,
else => unreachable,
}
}
os.munmap(self.data.memory);
},
.windows => {
windows.WaitForSingleObjectEx(self.data.handle, windows.INFINITE, false) catch unreachable;
windows.CloseHandle(self.data.handle);
windows.HeapFree(self.data.heap_handle, 0, self.data.alloc_start);
},
else => @compileError("Unsupported OS"),
}
}
pub const SpawnError = error{
/// A system-imposed limit on the number of threads was encountered.
/// There are a number of limits that may trigger this error:
/// * the RLIMIT_NPROC soft resource limit (set via setrlimit(2)),
/// which limits the number of processes and threads for a real
/// user ID, was reached;
/// * the kernel's system-wide limit on the number of processes and
/// threads, /proc/sys/kernel/threads-max, was reached (see
/// proc(5));
/// * the maximum number of PIDs, /proc/sys/kernel/pid_max, was
/// reached (see proc(5)); or
/// * the PID limit (pids.max) imposed by the cgroup "process num
/// ber" (PIDs) controller was reached.
ThreadQuotaExceeded,
/// The kernel cannot allocate sufficient memory to allocate a task structure
/// for the child, or to copy those parts of the caller's context that need to
/// be copied.
SystemResources,
/// Not enough userland memory to spawn the thread.
OutOfMemory,
/// `mlockall` is enabled, and the memory needed to spawn the thread
/// would exceed the limit.
LockedMemoryLimitExceeded,
Unexpected,
};
/// caller must call wait on the returned thread
/// fn startFn(@TypeOf(context)) T
/// where T is u8, noreturn, void, or !void
/// caller must call wait on the returned thread
pub fn spawn(context: anytype, comptime startFn: anytype) SpawnError!*Thread {
if (builtin.single_threaded) @compileError("cannot spawn thread when building in single-threaded mode");
// TODO compile-time call graph analysis to determine stack upper bound
// https://github.com/ziglang/zig/issues/157
const default_stack_size = 16 * 1024 * 1024;
const Context = @TypeOf(context);
comptime assert(@typeInfo(@TypeOf(startFn)).Fn.args[0].arg_type.? == Context);
if (std.Target.current.os.tag == .windows) {
const WinThread = struct {
const OuterContext = struct {
thread: Thread,
inner: Context,
};
fn threadMain(raw_arg: windows.LPVOID) callconv(.C) windows.DWORD {
const arg = if (@sizeOf(Context) == 0) {} else @ptrCast(*Context, @alignCast(@alignOf(Context), raw_arg)).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return 0;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
return startFn(arg);
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return 0;
},
else => @compileError(bad_startfn_ret),
}
}
};
const heap_handle = windows.kernel32.GetProcessHeap() orelse return error.OutOfMemory;
const byte_count = @alignOf(WinThread.OuterContext) + @sizeOf(WinThread.OuterContext);
const bytes_ptr = windows.kernel32.HeapAlloc(heap_handle, 0, byte_count) orelse return error.OutOfMemory;
errdefer assert(windows.kernel32.HeapFree(heap_handle, 0, bytes_ptr) != 0);
const bytes = @ptrCast([*]u8, bytes_ptr)[0..byte_count];
const outer_context = std.heap.FixedBufferAllocator.init(bytes).allocator.create(WinThread.OuterContext) catch unreachable;
outer_context.* = WinThread.OuterContext{
.thread = Thread{
.data = Thread.Data{
.heap_handle = heap_handle,
.alloc_start = bytes_ptr,
.handle = undefined,
},
},
.inner = context,
};
const parameter = if (@sizeOf(Context) == 0) null else @ptrCast(*c_void, &outer_context.inner);
outer_context.thread.data.handle = windows.kernel32.CreateThread(null, default_stack_size, WinThread.threadMain, parameter, 0, null) orelse {
switch (windows.kernel32.GetLastError()) {
else => |err| return windows.unexpectedError(err),
}
};
return &outer_context.thread;
}
const MainFuncs = struct {
fn linuxThreadMain(ctx_addr: usize) callconv(.C) u8 {
const arg = if (@sizeOf(Context) == 0) {} else @intToPtr(*const Context, ctx_addr).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return 0;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
return startFn(arg);
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return 0;
},
else => @compileError(bad_startfn_ret),
}
}
fn posixThreadMain(ctx: ?*c_void) callconv(.C) ?*c_void {
const arg = if (@sizeOf(Context) == 0) {} else @ptrCast(*Context, @alignCast(@alignOf(Context), ctx)).*;
switch (@typeInfo(@typeInfo(@TypeOf(startFn)).Fn.return_type.?)) {
.NoReturn => {
startFn(arg);
},
.Void => {
startFn(arg);
return null;
},
.Int => |info| {
if (info.bits != 8) {
@compileError(bad_startfn_ret);
}
// pthreads don't support exit status, ignore value
_ = startFn(arg);
return null;
},
.ErrorUnion => |info| {
if (info.payload != void) {
@compileError(bad_startfn_ret);
}
startFn(arg) catch |err| {
std.debug.warn("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
};
return null;
},
else => @compileError(bad_startfn_ret),
}
}
};
if (Thread.use_pthreads) {
var attr: c.pthread_attr_t = undefined;
if (c.pthread_attr_init(&attr) != 0) return error.SystemResources;
defer assert(c.pthread_attr_destroy(&attr) == 0);
const thread_obj = try std.heap.c_allocator.create(Thread);
errdefer std.heap.c_allocator.destroy(thread_obj);
if (@sizeOf(Context) > 0) {
thread_obj.data.memory = try std.heap.c_allocator.allocAdvanced(
u8,
@alignOf(Context),
@sizeOf(Context),
.at_least,
);
errdefer std.heap.c_allocator.free(thread_obj.data.memory);
mem.copy(u8, thread_obj.data.memory, mem.asBytes(&context));
} else {
thread_obj.data.memory = @as([*]u8, undefined)[0..0];
}
// Use the same set of parameters used by the libc-less impl.
assert(c.pthread_attr_setstacksize(&attr, default_stack_size) == 0);
assert(c.pthread_attr_setguardsize(&attr, mem.page_size) == 0);
const err = c.pthread_create(
&thread_obj.data.handle,
&attr,
MainFuncs.posixThreadMain,
thread_obj.data.memory.ptr,
);
switch (err) {
0 => return thread_obj,
os.EAGAIN => return error.SystemResources,
os.EPERM => unreachable,
os.EINVAL => unreachable,
else => return os.unexpectedErrno(@intCast(usize, err)),
}
return thread_obj;
}
var guard_end_offset: usize = undefined;
var stack_end_offset: usize = undefined;
var thread_start_offset: usize = undefined;
var context_start_offset: usize = undefined;
var tls_start_offset: usize = undefined;
const mmap_len = blk: {
var l: usize = mem.page_size;
// Allocate a guard page right after the end of the stack region
guard_end_offset = l;
// The stack itself, which grows downwards.
l = mem.alignForward(l + default_stack_size, mem.page_size);
stack_end_offset = l;
// Above the stack, so that it can be in the same mmap call, put the Thread object.
l = mem.alignForward(l, @alignOf(Thread));
thread_start_offset = l;
l += @sizeOf(Thread);
// Next, the Context object.
if (@sizeOf(Context) != 0) {
l = mem.alignForward(l, @alignOf(Context));
context_start_offset = l;
l += @sizeOf(Context);
}
// Finally, the Thread Local Storage, if any.
l = mem.alignForward(l, os.linux.tls.tls_image.alloc_align);
tls_start_offset = l;
l += os.linux.tls.tls_image.alloc_size;
// Round the size to the page size.
break :blk mem.alignForward(l, mem.page_size);
};
const mmap_slice = mem: {
// Map the whole stack with no rw permissions to avoid
// committing the whole region right away
const mmap_slice = os.mmap(
null,
mmap_len,
os.PROT_NONE,
os.MAP_PRIVATE | os.MAP_ANONYMOUS,
-1,
0,
) catch |err| switch (err) {
error.MemoryMappingNotSupported => unreachable,
error.AccessDenied => unreachable,
error.PermissionDenied => unreachable,
else => |e| return e,
};
errdefer os.munmap(mmap_slice);
// Map everything but the guard page as rw
os.mprotect(
mmap_slice[guard_end_offset..],
os.PROT_READ | os.PROT_WRITE,
) catch |err| switch (err) {
error.AccessDenied => unreachable,
else => |e| return e,
};
break :mem mmap_slice;
};
const mmap_addr = @ptrToInt(mmap_slice.ptr);
const thread_ptr = @alignCast(@alignOf(Thread), @intToPtr(*Thread, mmap_addr + thread_start_offset));
thread_ptr.data.memory = mmap_slice;
var arg: usize = undefined;
if (@sizeOf(Context) != 0) {
arg = mmap_addr + context_start_offset;
const context_ptr = @alignCast(@alignOf(Context), @intToPtr(*Context, arg));
context_ptr.* = context;
}
if (std.Target.current.os.tag == .linux) {
const flags: u32 = os.CLONE_VM | os.CLONE_FS | os.CLONE_FILES |
os.CLONE_SIGHAND | os.CLONE_THREAD | os.CLONE_SYSVSEM |
os.CLONE_PARENT_SETTID | os.CLONE_CHILD_CLEARTID |
os.CLONE_DETACHED | os.CLONE_SETTLS;
// This structure is only needed when targeting i386
var user_desc: if (std.Target.current.cpu.arch == .i386) os.linux.user_desc else void = undefined;
const tls_area = mmap_slice[tls_start_offset..];
const tp_value = os.linux.tls.prepareTLS(tls_area);
const newtls = blk: {
if (std.Target.current.cpu.arch == .i386) {
user_desc = os.linux.user_desc{
.entry_number = os.linux.tls.tls_image.gdt_entry_number,
.base_addr = tp_value,
.limit = 0xfffff,
.seg_32bit = 1,
.contents = 0, // Data
.read_exec_only = 0,
.limit_in_pages = 1,
.seg_not_present = 0,
.useable = 1,
};
break :blk @ptrToInt(&user_desc);
} else {
break :blk tp_value;
}
};
const rc = os.linux.clone(
MainFuncs.linuxThreadMain,
mmap_addr + stack_end_offset,
flags,
arg,
&thread_ptr.data.handle,
newtls,
&thread_ptr.data.handle,
);
switch (os.errno(rc)) {
0 => return thread_ptr,
os.EAGAIN => return error.ThreadQuotaExceeded,
os.EINVAL => unreachable,
os.ENOMEM => return error.SystemResources,
os.ENOSPC => unreachable,
os.EPERM => unreachable,
os.EUSERS => unreachable,
else => |err| return os.unexpectedErrno(err),
}
} else {
@compileError("Unsupported OS");
}
}
pub const CpuCountError = error{
PermissionDenied,
SystemResources,
Unexpected,
};
pub fn cpuCount() CpuCountError!usize {
if (std.Target.current.os.tag == .linux) {
const cpu_set = try os.sched_getaffinity(0);
return @as(usize, os.CPU_COUNT(cpu_set)); // TODO should not need this usize cast
}
if (std.Target.current.os.tag == .windows) {
return os.windows.peb().NumberOfProcessors;
}
if (std.Target.current.os.tag == .openbsd) {
var count: c_int = undefined;
var count_size: usize = @sizeOf(c_int);
const mib = [_]c_int{ os.CTL_HW, os.HW_NCPUONLINE };
os.sysctl(&mib, &count, &count_size, null, 0) catch |err| switch (err) {
error.NameTooLong, error.UnknownName => unreachable,
else => |e| return e,
};
return @intCast(usize, count);
}
var count: c_int = undefined;
var count_len: usize = @sizeOf(c_int);
const name = if (comptime std.Target.current.isDarwin()) "hw.logicalcpu" else "hw.ncpu";
os.sysctlbynameZ(name, &count, &count_len, null, 0) catch |err| switch (err) {
error.NameTooLong, error.UnknownName => unreachable,
else => |e| return e,
};
return @intCast(usize, count);
}
pub fn getCurrentThreadId() u64 {
switch (std.Target.current.os.tag) {
.linux => {
// Use the syscall directly as musl doesn't provide a wrapper.
return @bitCast(u32, os.linux.gettid());
},
.windows => {
return os.windows.kernel32.GetCurrentThreadId();
},
.macos, .ios, .watchos, .tvos => {
var thread_id: u64 = undefined;
// Pass thread=null to get the current thread ID.
assert(c.pthread_threadid_np(null, &thread_id) == 0);
return thread_id;
},
.netbsd => {
return @bitCast(u32, c._lwp_self());
},
.freebsd => {
return @bitCast(u32, c.pthread_getthreadid_np());
},
.openbsd => {
return @bitCast(u32, c.getthrid());
},
else => {
@compileError("getCurrentThreadId not implemented for this platform");
},
}
}
};

View File

@ -125,7 +125,7 @@ owned_link_dir: ?std.fs.Dir,
color: @import("main.zig").Color = .auto, color: @import("main.zig").Color = .auto,
/// This mutex guards all `Compilation` mutable state. /// This mutex guards all `Compilation` mutable state.
mutex: std.Mutex = .{}, mutex: std.Thread.Mutex = .{},
test_filter: ?[]const u8, test_filter: ?[]const u8,
test_name_prefix: ?[]const u8, test_name_prefix: ?[]const u8,

View File

@ -6,14 +6,14 @@
const std = @import("std"); const std = @import("std");
const ThreadPool = @This(); const ThreadPool = @This();
lock: std.Mutex = .{}, lock: std.Thread.Mutex = .{},
is_running: bool = true, is_running: bool = true,
allocator: *std.mem.Allocator, allocator: *std.mem.Allocator,
workers: []Worker, workers: []Worker,
run_queue: RunQueue = .{}, run_queue: RunQueue = .{},
idle_queue: IdleQueue = .{}, idle_queue: IdleQueue = .{},
const IdleQueue = std.SinglyLinkedList(std.ResetEvent); const IdleQueue = std.SinglyLinkedList(std.Thread.ResetEvent);
const RunQueue = std.SinglyLinkedList(Runnable); const RunQueue = std.SinglyLinkedList(Runnable);
const Runnable = struct { const Runnable = struct {
runFn: fn (*Runnable) void, runFn: fn (*Runnable) void,

View File

@ -6,9 +6,9 @@
const std = @import("std"); const std = @import("std");
const WaitGroup = @This(); const WaitGroup = @This();
lock: std.Mutex = .{}, lock: std.Thread.Mutex = .{},
counter: usize = 0, counter: usize = 0,
event: std.ResetEvent, event: std.Thread.ResetEvent,
pub fn init(self: *WaitGroup) !void { pub fn init(self: *WaitGroup) !void {
self.* = .{ self.* = .{