//! 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. const std = @import("std.zig"); const builtin = @import("builtin"); const math = std.math; const os = std.os; const assert = std.debug.assert; const target = builtin.target; pub const Futex = @import("Thread/Futex.zig"); pub const ResetEvent = @import("Thread/ResetEvent.zig"); pub const Mutex = @import("Thread/Mutex.zig"); pub const Semaphore = @import("Thread/Semaphore.zig"); pub const Condition = @import("Thread/Condition.zig"); pub const RwLock = @import("Thread/RwLock.zig"); pub const Pool = @import("Thread/Pool.zig"); pub const WaitGroup = @import("Thread/WaitGroup.zig"); pub const use_pthreads = target.os.tag != .windows and target.os.tag != .wasi and builtin.link_libc; const Thread = @This(); const Impl = if (target.os.tag == .windows) WindowsThreadImpl else if (use_pthreads) PosixThreadImpl else if (target.os.tag == .linux) LinuxThreadImpl else if (target.os.tag == .wasi) WasiThreadImpl else UnsupportedImpl; impl: Impl, pub const max_name_len = switch (target.os.tag) { .linux => 15, .windows => 31, .macos, .ios, .watchos, .tvos => 63, .netbsd => 31, .freebsd => 15, .openbsd => 23, .dragonfly => 1023, .solaris, .illumos => 31, else => 0, }; pub const SetNameError = error{ NameTooLong, Unsupported, Unexpected, } || os.PrctlError || os.WriteError || std.fs.File.OpenError || std.fmt.BufPrintError; pub fn setName(self: Thread, name: []const u8) SetNameError!void { if (name.len > max_name_len) return error.NameTooLong; const name_with_terminator = blk: { var name_buf: [max_name_len:0]u8 = undefined; @memcpy(name_buf[0..name.len], name); name_buf[name.len] = 0; break :blk name_buf[0..name.len :0]; }; switch (target.os.tag) { .linux => if (use_pthreads) { if (self.getHandle() == std.c.pthread_self()) { // Set the name of the calling thread (no thread id required). const err = try os.prctl(.SET_NAME, .{@intFromPtr(name_with_terminator.ptr)}); switch (@as(os.E, @enumFromInt(err))) { .SUCCESS => return, else => |e| return os.unexpectedErrno(e), } } else { const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr); switch (err) { .SUCCESS => return, .RANGE => unreachable, else => |e| return os.unexpectedErrno(e), } } } else { var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); const file = try std.fs.cwd().openFile(path, .{ .mode = .write_only }); defer file.close(); try file.writer().writeAll(name); return; }, .windows => { var buf: [max_name_len]u16 = undefined; const len = try std.unicode.utf8ToUtf16Le(&buf, name); const byte_len = math.cast(c_ushort, len * 2) orelse return error.NameTooLong; // Note: NT allocates its own copy, no use-after-free here. const unicode_string = os.windows.UNICODE_STRING{ .Length = byte_len, .MaximumLength = byte_len, .Buffer = &buf, }; switch (os.windows.ntdll.NtSetInformationThread( self.getHandle(), .ThreadNameInformation, &unicode_string, @sizeOf(os.windows.UNICODE_STRING), )) { .SUCCESS => return, .NOT_IMPLEMENTED => return error.Unsupported, else => |err| return os.windows.unexpectedStatus(err), } }, .macos, .ios, .watchos, .tvos => if (use_pthreads) { // There doesn't seem to be a way to set the name for an arbitrary thread, only the current one. if (self.getHandle() != std.c.pthread_self()) return error.Unsupported; const err = std.c.pthread_setname_np(name_with_terminator.ptr); switch (err) { .SUCCESS => return, else => |e| return os.unexpectedErrno(e), } }, .netbsd, .solaris, .illumos => if (use_pthreads) { const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr, null); switch (err) { .SUCCESS => return, .INVAL => unreachable, .SRCH => unreachable, .NOMEM => unreachable, else => |e| return os.unexpectedErrno(e), } }, .freebsd, .openbsd => if (use_pthreads) { // Use pthread_set_name_np for FreeBSD because pthread_setname_np is FreeBSD 12.2+ only. // TODO maybe revisit this if depending on FreeBSD 12.2+ is acceptable because // pthread_setname_np can return an error. std.c.pthread_set_name_np(self.getHandle(), name_with_terminator.ptr); return; }, .dragonfly => if (use_pthreads) { const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr); switch (err) { .SUCCESS => return, .INVAL => unreachable, .FAULT => unreachable, .NAMETOOLONG => unreachable, // already checked .SRCH => unreachable, else => |e| return os.unexpectedErrno(e), } }, else => {}, } return error.Unsupported; } pub const GetNameError = error{ // For Windows, the name is converted from UTF16 to UTF8 CodepointTooLarge, Utf8CannotEncodeSurrogateHalf, DanglingSurrogateHalf, ExpectedSecondSurrogateHalf, UnexpectedSecondSurrogateHalf, Unsupported, Unexpected, } || os.PrctlError || os.ReadError || std.fs.File.OpenError || std.fmt.BufPrintError; pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]const u8 { buffer_ptr[max_name_len] = 0; var buffer: [:0]u8 = buffer_ptr; switch (target.os.tag) { .linux => if (use_pthreads) { if (self.getHandle() == std.c.pthread_self()) { // Get the name of the calling thread (no thread id required). const err = try os.prctl(.GET_NAME, .{@intFromPtr(buffer.ptr)}); switch (@as(os.E, @enumFromInt(err))) { .SUCCESS => return std.mem.sliceTo(buffer, 0), else => |e| return os.unexpectedErrno(e), } } else { const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); switch (err) { .SUCCESS => return std.mem.sliceTo(buffer, 0), .RANGE => unreachable, else => |e| return os.unexpectedErrno(e), } } } else { var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const data_len = try file.reader().readAll(buffer_ptr[0 .. max_name_len + 1]); return if (data_len >= 1) buffer[0 .. data_len - 1] else null; }, .windows => { const buf_capacity = @sizeOf(os.windows.UNICODE_STRING) + (@sizeOf(u16) * max_name_len); var buf: [buf_capacity]u8 align(@alignOf(os.windows.UNICODE_STRING)) = undefined; switch (os.windows.ntdll.NtQueryInformationThread( self.getHandle(), .ThreadNameInformation, &buf, buf_capacity, null, )) { .SUCCESS => { const string = @as(*const os.windows.UNICODE_STRING, @ptrCast(&buf)); const len = try std.unicode.utf16leToUtf8(buffer, string.Buffer[0 .. string.Length / 2]); return if (len > 0) buffer[0..len] else null; }, .NOT_IMPLEMENTED => return error.Unsupported, else => |err| return os.windows.unexpectedStatus(err), } }, .macos, .ios, .watchos, .tvos => if (use_pthreads) { const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); switch (err) { .SUCCESS => return std.mem.sliceTo(buffer, 0), .SRCH => unreachable, else => |e| return os.unexpectedErrno(e), } }, .netbsd, .solaris, .illumos => if (use_pthreads) { const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); switch (err) { .SUCCESS => return std.mem.sliceTo(buffer, 0), .INVAL => unreachable, .SRCH => unreachable, else => |e| return os.unexpectedErrno(e), } }, .freebsd, .openbsd => if (use_pthreads) { // Use pthread_get_name_np for FreeBSD because pthread_getname_np is FreeBSD 12.2+ only. // TODO maybe revisit this if depending on FreeBSD 12.2+ is acceptable because pthread_getname_np can return an error. std.c.pthread_get_name_np(self.getHandle(), buffer.ptr, max_name_len + 1); return std.mem.sliceTo(buffer, 0); }, .dragonfly => if (use_pthreads) { const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); switch (err) { .SUCCESS => return std.mem.sliceTo(buffer, 0), .INVAL => unreachable, .FAULT => unreachable, .SRCH => unreachable, else => |e| return os.unexpectedErrno(e), } }, else => {}, } return error.Unsupported; } /// Represents an ID per thread guaranteed to be unique only within a process. pub const Id = switch (target.os.tag) { .linux, .dragonfly, .netbsd, .freebsd, .openbsd, .haiku, .wasi, => u32, .macos, .ios, .watchos, .tvos => u64, .windows => os.windows.DWORD, else => usize, }; /// Returns the platform ID of the callers thread. /// Attempts to use thread locals and avoid syscalls when possible. pub fn getCurrentId() Id { return Impl.getCurrentId(); } pub const CpuCountError = error{ PermissionDenied, SystemResources, Unexpected, }; /// Returns the platforms view on the number of logical CPU cores available. pub fn getCpuCount() CpuCountError!usize { return Impl.getCpuCount(); } /// Configuration options for hints on how to spawn threads. pub const SpawnConfig = struct { // TODO compile-time call graph analysis to determine stack upper bound // https://github.com/ziglang/zig/issues/157 /// Size in bytes of the Thread's stack stack_size: usize = 16 * 1024 * 1024, /// The allocator to be used to allocate memory for the to-be-spawned thread allocator: ?std.mem.Allocator = null, }; 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, }; /// Spawns a new thread which executes `function` using `args` and returns a handle to the spawned thread. /// `config` can be used as hints to the platform for how to spawn and execute the `function`. /// The caller must eventually either call `join()` to wait for the thread to finish and free its resources /// or call `detach()` to excuse the caller from calling `join()` and have the thread clean up its resources on completion. pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread { if (builtin.single_threaded) { @compileError("Cannot spawn thread when building in single-threaded mode"); } const impl = try Impl.spawn(config, function, args); return Thread{ .impl = impl }; } /// Represents a kernel thread handle. /// May be an integer or a pointer depending on the platform. pub const Handle = Impl.ThreadHandle; /// Returns the handle of this thread pub fn getHandle(self: Thread) Handle { return self.impl.getHandle(); } /// Release the obligation of the caller to call `join()` and have the thread clean up its own resources on completion. /// Once called, this consumes the Thread object and invoking any other functions on it is considered undefined behavior. pub fn detach(self: Thread) void { return self.impl.detach(); } /// Waits for the thread to complete, then deallocates any resources created on `spawn()`. /// Once called, this consumes the Thread object and invoking any other functions on it is considered undefined behavior. pub fn join(self: Thread) void { return self.impl.join(); } pub const YieldError = error{ /// The system is not configured to allow yielding SystemCannotYield, }; /// Yields the current thread potentially allowing other threads to run. pub fn yield() YieldError!void { if (builtin.os.tag == .windows) { // The return value has to do with how many other threads there are; it is not // an error condition on Windows. _ = os.windows.kernel32.SwitchToThread(); return; } switch (os.errno(os.system.sched_yield())) { .SUCCESS => return, .NOSYS => return error.SystemCannotYield, else => return error.SystemCannotYield, } } /// State to synchronize detachment of spawner thread to spawned thread const Completion = std.atomic.Value(enum(u8) { running, detached, completed, }); /// Used by the Thread implementations to call the spawned function with the arguments. fn callFn(comptime f: anytype, args: anytype) switch (Impl) { WindowsThreadImpl => std.os.windows.DWORD, LinuxThreadImpl => u8, PosixThreadImpl => ?*anyopaque, else => unreachable, } { const default_value = if (Impl == PosixThreadImpl) null else 0; const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', 'void', or '!void'"; switch (@typeInfo(@typeInfo(@TypeOf(f)).Fn.return_type.?)) { .NoReturn => { @call(.auto, f, args); }, .Void => { @call(.auto, f, args); return default_value; }, .Int => |info| { if (info.bits != 8) { @compileError(bad_fn_ret); } const status = @call(.auto, f, args); if (Impl != PosixThreadImpl) { return status; } // pthreads don't support exit status, ignore value return default_value; }, .ErrorUnion => |info| { if (info.payload != void) { @compileError(bad_fn_ret); } @call(.auto, f, args) catch |err| { std.debug.print("error: {s}\n", .{@errorName(err)}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } }; return default_value; }, else => { @compileError(bad_fn_ret); }, } } /// We can't compile error in the `Impl` switch statement as its eagerly evaluated. /// So instead, we compile-error on the methods themselves for platforms which don't support threads. const UnsupportedImpl = struct { pub const ThreadHandle = void; fn getCurrentId() usize { return unsupported({}); } fn getCpuCount() !usize { return unsupported({}); } fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { return unsupported(.{ config, f, args }); } fn getHandle(self: Impl) ThreadHandle { return unsupported(self); } fn detach(self: Impl) void { return unsupported(self); } fn join(self: Impl) void { return unsupported(self); } fn unsupported(unused: anytype) noreturn { _ = unused; @compileError("Unsupported operating system " ++ @tagName(target.os.tag)); } }; const WindowsThreadImpl = struct { const windows = os.windows; pub const ThreadHandle = windows.HANDLE; fn getCurrentId() windows.DWORD { return windows.kernel32.GetCurrentThreadId(); } fn getCpuCount() !usize { // Faster than calling into GetSystemInfo(), even if amortized. return windows.peb().NumberOfProcessors; } thread: *ThreadCompletion, const ThreadCompletion = struct { completion: Completion, heap_ptr: windows.PVOID, heap_handle: windows.HANDLE, thread_handle: windows.HANDLE = undefined, fn free(self: ThreadCompletion) void { const status = windows.kernel32.HeapFree(self.heap_handle, 0, self.heap_ptr); assert(status != 0); } }; fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { const Args = @TypeOf(args); const Instance = struct { fn_args: Args, thread: ThreadCompletion, fn entryFn(raw_ptr: windows.PVOID) callconv(.C) windows.DWORD { const self: *@This() = @ptrCast(@alignCast(raw_ptr)); defer switch (self.thread.completion.swap(.completed, .SeqCst)) { .running => {}, .completed => unreachable, .detached => self.thread.free(), }; return callFn(f, self.fn_args); } }; const heap_handle = windows.kernel32.GetProcessHeap() orelse return error.OutOfMemory; const alloc_bytes = @alignOf(Instance) + @sizeOf(Instance); const alloc_ptr = windows.kernel32.HeapAlloc(heap_handle, 0, alloc_bytes) orelse return error.OutOfMemory; errdefer assert(windows.kernel32.HeapFree(heap_handle, 0, alloc_ptr) != 0); const instance_bytes = @as([*]u8, @ptrCast(alloc_ptr))[0..alloc_bytes]; var fba = std.heap.FixedBufferAllocator.init(instance_bytes); const instance = fba.allocator().create(Instance) catch unreachable; instance.* = .{ .fn_args = args, .thread = .{ .completion = Completion.init(.running), .heap_ptr = alloc_ptr, .heap_handle = heap_handle, }, }; // Windows appears to only support SYSTEM_INFO.dwAllocationGranularity minimum stack size. // Going lower makes it default to that specified in the executable (~1mb). // Its also fine if the limit here is incorrect as stack size is only a hint. var stack_size = std.math.cast(u32, config.stack_size) orelse std.math.maxInt(u32); stack_size = @max(64 * 1024, stack_size); instance.thread.thread_handle = windows.kernel32.CreateThread( null, stack_size, Instance.entryFn, instance, 0, null, ) orelse { const errno = windows.kernel32.GetLastError(); return windows.unexpectedError(errno); }; return Impl{ .thread = &instance.thread }; } fn getHandle(self: Impl) ThreadHandle { return self.thread.thread_handle; } fn detach(self: Impl) void { windows.CloseHandle(self.thread.thread_handle); switch (self.thread.completion.swap(.detached, .SeqCst)) { .running => {}, .completed => self.thread.free(), .detached => unreachable, } } fn join(self: Impl) void { windows.WaitForSingleObjectEx(self.thread.thread_handle, windows.INFINITE, false) catch unreachable; windows.CloseHandle(self.thread.thread_handle); assert(self.thread.completion.load(.SeqCst) == .completed); self.thread.free(); } }; const PosixThreadImpl = struct { const c = std.c; pub const ThreadHandle = c.pthread_t; fn getCurrentId() Id { switch (target.os.tag) { .linux => { return LinuxThreadImpl.getCurrentId(); }, .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; }, .dragonfly => { return @as(u32, @bitCast(c.lwp_gettid())); }, .netbsd => { return @as(u32, @bitCast(c._lwp_self())); }, .freebsd => { return @as(u32, @bitCast(c.pthread_getthreadid_np())); }, .openbsd => { return @as(u32, @bitCast(c.getthrid())); }, .haiku => { return @as(u32, @bitCast(c.find_thread(null))); }, else => { return @intFromPtr(c.pthread_self()); }, } } fn getCpuCount() !usize { switch (target.os.tag) { .linux => { return LinuxThreadImpl.getCpuCount(); }, .openbsd => { var count: c_int = undefined; var count_size: usize = @sizeOf(c_int); const mib = [_]c_int{ os.CTL.HW, os.system.HW.NCPUONLINE }; os.sysctl(&mib, &count, &count_size, null, 0) catch |err| switch (err) { error.NameTooLong, error.UnknownName => unreachable, else => |e| return e, }; return @as(usize, @intCast(count)); }, .solaris, .illumos => { // The "proper" way to get the cpu count would be to query // /dev/kstat via ioctls, and traverse a linked list for each // cpu. const rc = c.sysconf(os._SC.NPROCESSORS_ONLN); return switch (os.errno(rc)) { .SUCCESS => @as(usize, @intCast(rc)), else => |err| os.unexpectedErrno(err), }; }, .haiku => { var system_info: os.system.system_info = undefined; const rc = os.system.get_system_info(&system_info); // always returns B_OK return switch (os.errno(rc)) { .SUCCESS => @as(usize, @intCast(system_info.cpu_count)), else => |err| os.unexpectedErrno(err), }; }, else => { var count: c_int = undefined; var count_len: usize = @sizeOf(c_int); const name = if (comptime target.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 @as(usize, @intCast(count)); }, } } handle: ThreadHandle, fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { const Args = @TypeOf(args); const allocator = std.heap.c_allocator; const Instance = struct { fn entryFn(raw_arg: ?*anyopaque) callconv(.C) ?*anyopaque { const args_ptr: *Args = @ptrCast(@alignCast(raw_arg)); defer allocator.destroy(args_ptr); return callFn(f, args_ptr.*); } }; const args_ptr = try allocator.create(Args); args_ptr.* = args; errdefer allocator.destroy(args_ptr); var attr: c.pthread_attr_t = undefined; if (c.pthread_attr_init(&attr) != .SUCCESS) return error.SystemResources; defer assert(c.pthread_attr_destroy(&attr) == .SUCCESS); // Use the same set of parameters used by the libc-less impl. const stack_size = @max(config.stack_size, 16 * 1024); assert(c.pthread_attr_setstacksize(&attr, stack_size) == .SUCCESS); assert(c.pthread_attr_setguardsize(&attr, std.mem.page_size) == .SUCCESS); var handle: c.pthread_t = undefined; switch (c.pthread_create( &handle, &attr, Instance.entryFn, @ptrCast(args_ptr), )) { .SUCCESS => return Impl{ .handle = handle }, .AGAIN => return error.SystemResources, .PERM => unreachable, .INVAL => unreachable, else => |err| return os.unexpectedErrno(err), } } fn getHandle(self: Impl) ThreadHandle { return self.handle; } fn detach(self: Impl) void { switch (c.pthread_detach(self.handle)) { .SUCCESS => {}, .INVAL => unreachable, // thread handle is not joinable .SRCH => unreachable, // thread handle is invalid else => unreachable, } } fn join(self: Impl) void { switch (c.pthread_join(self.handle, null)) { .SUCCESS => {}, .INVAL => unreachable, // thread handle is not joinable (or another thread is already joining in) .SRCH => unreachable, // thread handle is invalid .DEADLK => unreachable, // two threads tried to join each other else => unreachable, } } }; const WasiThreadImpl = struct { thread: *WasiThread, pub const ThreadHandle = i32; threadlocal var tls_thread_id: Id = 0; const WasiThread = struct { /// Thread ID tid: std.atomic.Value(i32) = std.atomic.Value(i32).init(0), /// Contains all memory which was allocated to bootstrap this thread, including: /// - Guard page /// - Stack /// - TLS segment /// - `Instance` /// All memory is freed upon call to `join` memory: []u8, /// The allocator used to allocate the thread's memory, /// which is also used during `join` to ensure clean-up. allocator: std.mem.Allocator, /// The current state of the thread. state: State = State.init(.running), }; /// A meta-data structure used to bootstrap a thread const Instance = struct { thread: WasiThread, /// Contains the offset to the new __tls_base. /// The offset starting from the memory's base. tls_offset: usize, /// Contains the offset to the stack for the newly spawned thread. /// The offset is calculated starting from the memory's base. stack_offset: usize, /// Contains the raw pointer value to the wrapper which holds all arguments /// for the callback. raw_ptr: usize, /// Function pointer to a wrapping function which will call the user's /// function upon thread spawn. The above mentioned pointer will be passed /// to this function pointer as its argument. call_back: *const fn (usize) void, /// When a thread is in `detached` state, we must free all of its memory /// upon thread completion. However, as this is done while still within /// the thread, we must first jump back to the main thread's stack or else /// we end up freeing the stack that we're currently using. original_stack_pointer: [*]u8, }; const State = std.atomic.Value(enum(u8) { running, completed, detached }); fn getCurrentId() Id { return tls_thread_id; } fn getHandle(self: Impl) ThreadHandle { return self.thread.tid.load(.SeqCst); } fn detach(self: Impl) void { switch (self.thread.state.swap(.detached, .SeqCst)) { .running => {}, .completed => self.join(), .detached => unreachable, } } fn join(self: Impl) void { defer { // Create a copy of the allocator so we do not free the reference to the // original allocator while freeing the memory. var allocator = self.thread.allocator; allocator.free(self.thread.memory); } var spin: u8 = 10; while (true) { const tid = self.thread.tid.load(.SeqCst); if (tid == 0) { break; } if (spin > 0) { spin -= 1; std.atomic.spinLoopHint(); continue; } const result = asm ( \\ local.get %[ptr] \\ local.get %[expected] \\ i64.const -1 # infinite \\ memory.atomic.wait32 0 \\ local.set %[ret] : [ret] "=r" (-> u32), : [ptr] "r" (&self.thread.tid.value), [expected] "r" (tid), ); switch (result) { 0 => continue, // ok 1 => continue, // expected =! loaded 2 => unreachable, // timeout (infinite) else => unreachable, } } } fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) !WasiThreadImpl { if (config.allocator == null) return error.OutOfMemory; // an allocator is required to spawn a WASI-thread // Wrapping struct required to hold the user-provided function arguments. const Wrapper = struct { args: @TypeOf(args), fn entry(ptr: usize) void { const w: *@This() = @ptrFromInt(ptr); @call(.auto, f, w.args); } }; var stack_offset: usize = undefined; var tls_offset: usize = undefined; var wrapper_offset: usize = undefined; var instance_offset: usize = undefined; // Calculate the bytes we have to allocate to store all thread information, including: // - The actual stack for the thread // - The TLS segment // - `Instance` - containing information about how to call the user's function. const map_bytes = blk: { // start with atleast a single page, which is used as a guard to prevent // other threads clobbering our new thread. // Unfortunately, WebAssembly has no notion of read-only segments, so this // is only a best effort. var bytes: usize = std.wasm.page_size; bytes = std.mem.alignForward(usize, bytes, 16); // align stack to 16 bytes stack_offset = bytes; bytes += @max(std.wasm.page_size, config.stack_size); bytes = std.mem.alignForward(usize, bytes, __tls_align()); tls_offset = bytes; bytes += __tls_size(); bytes = std.mem.alignForward(usize, bytes, @alignOf(Wrapper)); wrapper_offset = bytes; bytes += @sizeOf(Wrapper); bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); instance_offset = bytes; bytes += @sizeOf(Instance); bytes = std.mem.alignForward(usize, bytes, std.wasm.page_size); break :blk bytes; }; // Allocate the amount of memory required for all meta data. const allocated_memory = try config.allocator.?.alloc(u8, map_bytes); const wrapper: *Wrapper = @ptrCast(@alignCast(&allocated_memory[wrapper_offset])); wrapper.* = .{ .args = args }; const instance: *Instance = @ptrCast(@alignCast(&allocated_memory[instance_offset])); instance.* = .{ .thread = .{ .memory = allocated_memory, .allocator = config.allocator.? }, .tls_offset = tls_offset, .stack_offset = stack_offset, .raw_ptr = @intFromPtr(wrapper), .call_back = &Wrapper.entry, .original_stack_pointer = __get_stack_pointer(), }; const tid = spawnWasiThread(instance); // The specification says any value lower than 0 indicates an error. // The values of such error are unspecified. WASI-Libc treats it as EAGAIN. if (tid < 0) { return error.SystemResources; } instance.thread.tid.store(tid, .SeqCst); return .{ .thread = &instance.thread }; } /// Bootstrap procedure, called by the host environment after thread creation. export fn wasi_thread_start(tid: i32, arg: *Instance) void { if (builtin.single_threaded) { // ensure function is not analyzed in single-threaded mode return; } __set_stack_pointer(arg.thread.memory.ptr + arg.stack_offset); __wasm_init_tls(arg.thread.memory.ptr + arg.tls_offset); @atomicStore(u32, &WasiThreadImpl.tls_thread_id, @intCast(tid), .SeqCst); // Finished bootstrapping, call user's procedure. arg.call_back(arg.raw_ptr); switch (arg.thread.state.swap(.completed, .SeqCst)) { .running => { // reset the Thread ID asm volatile ( \\ local.get %[ptr] \\ i32.const 0 \\ i32.atomic.store 0 : : [ptr] "r" (&arg.thread.tid.value), ); // Wake the main thread listening to this thread asm volatile ( \\ local.get %[ptr] \\ i32.const 1 # waiters \\ memory.atomic.notify 0 \\ drop # no need to know the waiters : : [ptr] "r" (&arg.thread.tid.value), ); }, .completed => unreachable, .detached => { // restore the original stack pointer so we can free the memory // without having to worry about freeing the stack __set_stack_pointer(arg.original_stack_pointer); // Ensure a copy so we don't free the allocator reference itself var allocator = arg.thread.allocator; allocator.free(arg.thread.memory); }, } } /// Asks the host to create a new thread for us. /// Newly created thread will call `wasi_tread_start` with the thread ID as well /// as the input `arg` that was provided to `spawnWasiThread` const spawnWasiThread = @"thread-spawn"; extern "wasi" fn @"thread-spawn"(arg: *Instance) i32; /// Initializes the TLS data segment starting at `memory`. /// This is a synthetic function, generated by the linker. extern fn __wasm_init_tls(memory: [*]u8) void; /// Returns a pointer to the base of the TLS data segment for the current thread inline fn __tls_base() [*]u8 { return asm ( \\ .globaltype __tls_base, i32 \\ global.get __tls_base \\ local.set %[ret] : [ret] "=r" (-> [*]u8), ); } /// Returns the size of the TLS segment inline fn __tls_size() u32 { return asm volatile ( \\ .globaltype __tls_size, i32, immutable \\ global.get __tls_size \\ local.set %[ret] : [ret] "=r" (-> u32), ); } /// Returns the alignment of the TLS segment inline fn __tls_align() u32 { return asm ( \\ .globaltype __tls_align, i32, immutable \\ global.get __tls_align \\ local.set %[ret] : [ret] "=r" (-> u32), ); } /// Allows for setting the stack pointer in the WebAssembly module. inline fn __set_stack_pointer(addr: [*]u8) void { asm volatile ( \\ local.get %[ptr] \\ global.set __stack_pointer : : [ptr] "r" (addr), ); } /// Returns the current value of the stack pointer inline fn __get_stack_pointer() [*]u8 { return asm ( \\ global.get __stack_pointer \\ local.set %[stack_ptr] : [stack_ptr] "=r" (-> [*]u8), ); } }; const LinuxThreadImpl = struct { const linux = os.linux; pub const ThreadHandle = i32; threadlocal var tls_thread_id: ?Id = null; fn getCurrentId() Id { return tls_thread_id orelse { const tid = @as(u32, @bitCast(linux.gettid())); tls_thread_id = tid; return tid; }; } fn getCpuCount() !usize { const cpu_set = try os.sched_getaffinity(0); // TODO: should not need this usize cast return @as(usize, os.CPU_COUNT(cpu_set)); } thread: *ThreadCompletion, const ThreadCompletion = struct { completion: Completion = Completion.init(.running), child_tid: std.atomic.Value(i32) = std.atomic.Value(i32).init(1), parent_tid: i32 = undefined, mapped: []align(std.mem.page_size) u8, /// Calls `munmap(mapped.ptr, mapped.len)` then `exit(1)` without touching the stack (which lives in `mapped.ptr`). /// Ported over from musl libc's pthread detached implementation: /// https://github.com/ifduyue/musl/search?q=__unmapself fn freeAndExit(self: *ThreadCompletion) noreturn { switch (target.cpu.arch) { .x86 => asm volatile ( \\ movl $91, %%eax \\ movl %[ptr], %%ebx \\ movl %[len], %%ecx \\ int $128 \\ movl $1, %%eax \\ movl $0, %%ebx \\ int $128 : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .x86_64 => asm volatile ( \\ movq $11, %%rax \\ syscall \\ movq $60, %%rax \\ movq $1, %%rdi \\ syscall : : [ptr] "{rdi}" (@intFromPtr(self.mapped.ptr)), [len] "{rsi}" (self.mapped.len), ), .arm, .armeb, .thumb, .thumbeb => asm volatile ( \\ mov r7, #91 \\ mov r0, %[ptr] \\ mov r1, %[len] \\ svc 0 \\ mov r7, #1 \\ mov r0, #0 \\ svc 0 : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .aarch64, .aarch64_be, .aarch64_32 => asm volatile ( \\ mov x8, #215 \\ mov x0, %[ptr] \\ mov x1, %[len] \\ svc 0 \\ mov x8, #93 \\ mov x0, #0 \\ svc 0 : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .mips, .mipsel => asm volatile ( \\ move $sp, $25 \\ li $2, 4091 \\ move $4, %[ptr] \\ move $5, %[len] \\ syscall \\ li $2, 4001 \\ li $4, 0 \\ syscall : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .mips64, .mips64el => asm volatile ( \\ li $2, 4091 \\ move $4, %[ptr] \\ move $5, %[len] \\ syscall \\ li $2, 4001 \\ li $4, 0 \\ syscall : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .powerpc, .powerpcle, .powerpc64, .powerpc64le => asm volatile ( \\ li 0, 91 \\ mr %[ptr], 3 \\ mr %[len], 4 \\ sc \\ li 0, 1 \\ li 3, 0 \\ sc \\ blr : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .riscv64 => asm volatile ( \\ li a7, 215 \\ mv a0, %[ptr] \\ mv a1, %[len] \\ ecall \\ li a7, 93 \\ mv a0, zero \\ ecall : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), .sparc64 => asm volatile ( \\ # SPARCs really don't like it when active stack frames \\ # is unmapped (it will result in a segfault), so we \\ # force-deactivate it by running `restore` until \\ # all frames are cleared. \\ 1: \\ cmp %%fp, 0 \\ beq 2f \\ nop \\ ba 1b \\ restore \\ 2: \\ mov 73, %%g1 \\ mov %[ptr], %%o0 \\ mov %[len], %%o1 \\ # Flush register window contents to prevent background \\ # memory access before unmapping the stack. \\ flushw \\ t 0x6d \\ mov 1, %%g1 \\ mov 1, %%o0 \\ t 0x6d : : [ptr] "r" (@intFromPtr(self.mapped.ptr)), [len] "r" (self.mapped.len), : "memory" ), else => |cpu_arch| @compileError("Unsupported linux arch: " ++ @tagName(cpu_arch)), } unreachable; } }; fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { const page_size = std.mem.page_size; const Args = @TypeOf(args); const Instance = struct { fn_args: Args, thread: ThreadCompletion, fn entryFn(raw_arg: usize) callconv(.C) u8 { const self = @as(*@This(), @ptrFromInt(raw_arg)); defer switch (self.thread.completion.swap(.completed, .SeqCst)) { .running => {}, .completed => unreachable, .detached => self.thread.freeAndExit(), }; return callFn(f, self.fn_args); } }; var guard_offset: usize = undefined; var stack_offset: usize = undefined; var tls_offset: usize = undefined; var instance_offset: usize = undefined; const map_bytes = blk: { var bytes: usize = page_size; guard_offset = bytes; bytes += @max(page_size, config.stack_size); bytes = std.mem.alignForward(usize, bytes, page_size); stack_offset = bytes; bytes = std.mem.alignForward(usize, bytes, linux.tls.tls_image.alloc_align); tls_offset = bytes; bytes += linux.tls.tls_image.alloc_size; bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); instance_offset = bytes; bytes += @sizeOf(Instance); bytes = std.mem.alignForward(usize, bytes, page_size); break :blk bytes; }; // map all memory needed without read/write permissions // to avoid committing the whole region right away // anonymous mapping ensures file descriptor limits are not exceeded const mapped = os.mmap( null, map_bytes, os.PROT.NONE, .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0, ) catch |err| switch (err) { error.MemoryMappingNotSupported => unreachable, error.AccessDenied => unreachable, error.PermissionDenied => unreachable, error.ProcessFdQuotaExceeded => unreachable, error.SystemFdQuotaExceeded => unreachable, else => |e| return e, }; assert(mapped.len >= map_bytes); errdefer os.munmap(mapped); // map everything but the guard page as read/write os.mprotect( @alignCast(mapped[guard_offset..]), os.PROT.READ | os.PROT.WRITE, ) catch |err| switch (err) { error.AccessDenied => unreachable, else => |e| return e, }; // Prepare the TLS segment and prepare a user_desc struct when needed on x86 var tls_ptr = os.linux.tls.prepareTLS(mapped[tls_offset..]); var user_desc: if (target.cpu.arch == .x86) os.linux.user_desc else void = undefined; if (target.cpu.arch == .x86) { defer tls_ptr = @intFromPtr(&user_desc); user_desc = .{ .entry_number = os.linux.tls.tls_image.gdt_entry_number, .base_addr = tls_ptr, .limit = 0xfffff, .flags = .{ .seg_32bit = 1, .contents = 0, // Data .read_exec_only = 0, .limit_in_pages = 1, .seg_not_present = 0, .useable = 1, }, }; } const instance: *Instance = @ptrCast(@alignCast(&mapped[instance_offset])); instance.* = .{ .fn_args = args, .thread = .{ .mapped = mapped }, }; const flags: u32 = linux.CLONE.THREAD | linux.CLONE.DETACHED | linux.CLONE.VM | linux.CLONE.FS | linux.CLONE.FILES | linux.CLONE.PARENT_SETTID | linux.CLONE.CHILD_CLEARTID | linux.CLONE.SIGHAND | linux.CLONE.SYSVSEM | linux.CLONE.SETTLS; switch (linux.getErrno(linux.clone( Instance.entryFn, @intFromPtr(&mapped[stack_offset]), flags, @intFromPtr(instance), &instance.thread.parent_tid, tls_ptr, &instance.thread.child_tid.raw, ))) { .SUCCESS => return Impl{ .thread = &instance.thread }, .AGAIN => return error.ThreadQuotaExceeded, .INVAL => unreachable, .NOMEM => return error.SystemResources, .NOSPC => unreachable, .PERM => unreachable, .USERS => unreachable, else => |err| return os.unexpectedErrno(err), } } fn getHandle(self: Impl) ThreadHandle { return self.thread.parent_tid; } fn detach(self: Impl) void { switch (self.thread.completion.swap(.detached, .SeqCst)) { .running => {}, .completed => self.join(), .detached => unreachable, } } fn join(self: Impl) void { defer os.munmap(self.thread.mapped); var spin: u8 = 10; while (true) { const tid = self.thread.child_tid.load(.SeqCst); if (tid == 0) { break; } if (spin > 0) { spin -= 1; std.atomic.spinLoopHint(); continue; } switch (linux.getErrno(linux.futex_wait( &self.thread.child_tid.raw, linux.FUTEX.WAIT, tid, null, ))) { .SUCCESS => continue, .INTR => continue, .AGAIN => continue, else => unreachable, } } } }; fn testThreadName(thread: *Thread) !void { const testCases = &[_][]const u8{ "mythread", "b" ** max_name_len, }; inline for (testCases) |tc| { try thread.setName(tc); var name_buffer: [max_name_len:0]u8 = undefined; const name = try thread.getName(&name_buffer); if (name) |value| { try std.testing.expectEqual(tc.len, value.len); try std.testing.expectEqualStrings(tc, value); } } } test "setName, getName" { if (builtin.single_threaded) return error.SkipZigTest; const Context = struct { start_wait_event: ResetEvent = .{}, test_done_event: ResetEvent = .{}, thread_done_event: ResetEvent = .{}, done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), thread: Thread = undefined, pub fn run(ctx: *@This()) !void { // Wait for the main thread to have set the thread field in the context. ctx.start_wait_event.wait(); switch (target.os.tag) { .windows => testThreadName(&ctx.thread) catch |err| switch (err) { error.Unsupported => return error.SkipZigTest, else => return err, }, else => try testThreadName(&ctx.thread), } // Signal our test is done ctx.test_done_event.set(); // wait for the thread to property exit ctx.thread_done_event.wait(); } }; var context = Context{}; var thread = try spawn(.{}, Context.run, .{&context}); context.thread = thread; context.start_wait_event.set(); context.test_done_event.wait(); switch (target.os.tag) { .macos, .ios, .watchos, .tvos => { const res = thread.setName("foobar"); try std.testing.expectError(error.Unsupported, res); }, .windows => testThreadName(&thread) catch |err| switch (err) { error.Unsupported => return error.SkipZigTest, else => return err, }, else => try testThreadName(&thread), } context.thread_done_event.set(); thread.join(); } test { // Doesn't use testing.refAllDecls() since that would pull in the compileError spinLoopHint. _ = Futex; _ = ResetEvent; _ = Mutex; _ = Semaphore; _ = Condition; _ = RwLock; } fn testIncrementNotify(value: *usize, event: *ResetEvent) void { value.* += 1; event.set(); } test "Thread.join" { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; var event = ResetEvent{}; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.join(); try std.testing.expectEqual(value, 1); } test "Thread.detach" { if (builtin.single_threaded) return error.SkipZigTest; var value: usize = 0; var event = ResetEvent{}; const thread = try Thread.spawn(.{}, testIncrementNotify, .{ &value, &event }); thread.detach(); event.wait(); try std.testing.expectEqual(value, 1); }