diff --git a/CMakeLists.txt b/CMakeLists.txt index 5afea9354e5a567bd354f895c9afd1cdc0a13fa0..c77c66add43884ac6fa556fad7b86596a26a96ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -506,7 +506,9 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/Thread.zig" "${CMAKE_SOURCE_DIR}/lib/std/Thread/Futex.zig" "${CMAKE_SOURCE_DIR}/lib/std/Thread/Mutex.zig" + "${CMAKE_SOURCE_DIR}/lib/std/Thread/Pool.zig" "${CMAKE_SOURCE_DIR}/lib/std/Thread/ResetEvent.zig" + "${CMAKE_SOURCE_DIR}/lib/std/Thread/WaitGroup.zig" "${CMAKE_SOURCE_DIR}/lib/std/time.zig" "${CMAKE_SOURCE_DIR}/lib/std/treap.zig" "${CMAKE_SOURCE_DIR}/lib/std/unicode.zig" @@ -530,9 +532,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/src/Package.zig" "${CMAKE_SOURCE_DIR}/src/RangeSet.zig" "${CMAKE_SOURCE_DIR}/src/Sema.zig" - "${CMAKE_SOURCE_DIR}/src/ThreadPool.zig" "${CMAKE_SOURCE_DIR}/src/TypedValue.zig" - "${CMAKE_SOURCE_DIR}/src/WaitGroup.zig" "${CMAKE_SOURCE_DIR}/src/Zir.zig" "${CMAKE_SOURCE_DIR}/src/arch/aarch64/CodeGen.zig" "${CMAKE_SOURCE_DIR}/src/arch/aarch64/Emit.zig" diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 27f7fa5030e964569c80e05ce3be2ba363facaaf..e3345e4a4200953d6258cf269abe2ae35009f63f 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -16,6 +16,8 @@ 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 is_gnu = target.abi.isGnu(); diff --git a/lib/std/Thread/Pool.zig b/lib/std/Thread/Pool.zig new file mode 100644 index 0000000000000000000000000000000000000000..930befbac5946625ff6a6eac0b849481b3b46364 --- /dev/null +++ b/lib/std/Thread/Pool.zig @@ -0,0 +1,152 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Pool = @This(); +const WaitGroup = @import("WaitGroup.zig"); + +mutex: std.Thread.Mutex = .{}, +cond: std.Thread.Condition = .{}, +run_queue: RunQueue = .{}, +is_running: bool = true, +allocator: std.mem.Allocator, +threads: []std.Thread, + +const RunQueue = std.SinglyLinkedList(Runnable); +const Runnable = struct { + runFn: RunProto, +}; + +const RunProto = *const fn (*Runnable) void; + +pub fn init(pool: *Pool, allocator: std.mem.Allocator) !void { + pool.* = .{ + .allocator = allocator, + .threads = &[_]std.Thread{}, + }; + + if (builtin.single_threaded) { + return; + } + + const thread_count = std.math.max(1, std.Thread.getCpuCount() catch 1); + pool.threads = try allocator.alloc(std.Thread, thread_count); + errdefer allocator.free(pool.threads); + + // kill and join any threads we spawned previously on error. + var spawned: usize = 0; + errdefer pool.join(spawned); + + for (pool.threads) |*thread| { + thread.* = try std.Thread.spawn(.{}, worker, .{pool}); + spawned += 1; + } +} + +pub fn deinit(pool: *Pool) void { + pool.join(pool.threads.len); // kill and join all threads. + pool.* = undefined; +} + +fn join(pool: *Pool, spawned: usize) void { + if (builtin.single_threaded) { + return; + } + + { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + // ensure future worker threads exit the dequeue loop + pool.is_running = false; + } + + // wake up any sleeping threads (this can be done outside the mutex) + // then wait for all the threads we know are spawned to complete. + pool.cond.broadcast(); + for (pool.threads[0..spawned]) |thread| { + thread.join(); + } + + pool.allocator.free(pool.threads); +} + +pub fn spawn(pool: *Pool, comptime func: anytype, args: anytype) !void { + if (builtin.single_threaded) { + @call(.auto, func, args); + return; + } + + const Args = @TypeOf(args); + const Closure = struct { + arguments: Args, + pool: *Pool, + run_node: RunQueue.Node = .{ .data = .{ .runFn = runFn } }, + + fn runFn(runnable: *Runnable) void { + const run_node = @fieldParentPtr(RunQueue.Node, "data", runnable); + const closure = @fieldParentPtr(@This(), "run_node", run_node); + @call(.auto, func, closure.arguments); + + // The thread pool's allocator is protected by the mutex. + const mutex = &closure.pool.mutex; + mutex.lock(); + defer mutex.unlock(); + + closure.pool.allocator.destroy(closure); + } + }; + + { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + const closure = try pool.allocator.create(Closure); + closure.* = .{ + .arguments = args, + .pool = pool, + }; + + pool.run_queue.prepend(&closure.run_node); + } + + // Notify waiting threads outside the lock to try and keep the critical section small. + pool.cond.signal(); +} + +fn worker(pool: *Pool) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + while (true) { + while (pool.run_queue.popFirst()) |run_node| { + // Temporarily unlock the mutex in order to execute the run_node + pool.mutex.unlock(); + defer pool.mutex.lock(); + + const runFn = run_node.data.runFn; + runFn(&run_node.data); + } + + // Stop executing instead of waiting if the thread pool is no longer running. + if (pool.is_running) { + pool.cond.wait(&pool.mutex); + } else { + break; + } + } +} + +pub fn waitAndWork(pool: *Pool, wait_group: *WaitGroup) void { + while (!wait_group.isDone()) { + if (blk: { + pool.mutex.lock(); + defer pool.mutex.unlock(); + break :blk pool.run_queue.popFirst(); + }) |run_node| { + run_node.data.runFn(&run_node.data); + continue; + } + + wait_group.wait(); + return; + } +} diff --git a/lib/std/Thread/WaitGroup.zig b/lib/std/Thread/WaitGroup.zig new file mode 100644 index 0000000000000000000000000000000000000000..c8be6658db76bc530f35bda4785274a3882bb319 --- /dev/null +++ b/lib/std/Thread/WaitGroup.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const Atomic = std.atomic.Atomic; +const assert = std.debug.assert; +const WaitGroup = @This(); + +const is_waiting: usize = 1 << 0; +const one_pending: usize = 1 << 1; + +state: Atomic(usize) = Atomic(usize).init(0), +event: std.Thread.ResetEvent = .{}, + +pub fn start(self: *WaitGroup) void { + const state = self.state.fetchAdd(one_pending, .Monotonic); + assert((state / one_pending) < (std.math.maxInt(usize) / one_pending)); +} + +pub fn finish(self: *WaitGroup) void { + const state = self.state.fetchSub(one_pending, .Release); + assert((state / one_pending) > 0); + + if (state == (one_pending | is_waiting)) { + self.state.fence(.Acquire); + self.event.set(); + } +} + +pub fn wait(self: *WaitGroup) void { + var state = self.state.fetchAdd(is_waiting, .Acquire); + assert(state & is_waiting == 0); + + if ((state / one_pending) > 0) { + self.event.wait(); + } +} + +pub fn reset(self: *WaitGroup) void { + self.state.store(0, .Monotonic); + self.event.reset(); +} + +pub fn isDone(wg: *WaitGroup) bool { + const state = wg.state.load(.Acquire); + assert(state & is_waiting == 0); + + return (state / one_pending) == 0; +} diff --git a/src/Compilation.zig b/src/Compilation.zig index de433a680075e9664e889353d4d1804320b3b987..63dd229ec5bcc8dadc8b45a5d4e445a994041214 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -7,6 +7,8 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const log = std.log.scoped(.compilation); const Target = std.Target; +const ThreadPool = std.Thread.Pool; +const WaitGroup = std.Thread.WaitGroup; const Value = @import("value.zig").Value; const Type = @import("type.zig").Type; @@ -30,8 +32,6 @@ const Cache = std.Build.Cache; const translate_c = @import("translate_c.zig"); const clang = @import("clang.zig"); const c_codegen = @import("codegen/c.zig"); -const ThreadPool = @import("ThreadPool.zig"); -const WaitGroup = @import("WaitGroup.zig"); const libtsan = @import("libtsan.zig"); const Zir = @import("Zir.zig"); const Autodoc = @import("Autodoc.zig"); diff --git a/src/Package.zig b/src/Package.zig index c238d3d567d70b39d480ebdff586a9961dbcd241..87d52197bd197a24024784ff4631008f601e9879 100644 --- a/src/Package.zig +++ b/src/Package.zig @@ -8,11 +8,11 @@ const Allocator = mem.Allocator; const assert = std.debug.assert; const log = std.log.scoped(.package); const main = @import("main.zig"); +const ThreadPool = std.Thread.Pool; +const WaitGroup = std.Thread.WaitGroup; const Compilation = @import("Compilation.zig"); const Module = @import("Module.zig"); -const ThreadPool = @import("ThreadPool.zig"); -const WaitGroup = @import("WaitGroup.zig"); const Cache = std.Build.Cache; const build_options = @import("build_options"); const Manifest = @import("Manifest.zig"); diff --git a/src/ThreadPool.zig b/src/ThreadPool.zig deleted file mode 100644 index fde5ed27db15adfaf0af5550f2f736fad87bb540..0000000000000000000000000000000000000000 --- a/src/ThreadPool.zig +++ /dev/null @@ -1,152 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const ThreadPool = @This(); -const WaitGroup = @import("WaitGroup.zig"); - -mutex: std.Thread.Mutex = .{}, -cond: std.Thread.Condition = .{}, -run_queue: RunQueue = .{}, -is_running: bool = true, -allocator: std.mem.Allocator, -threads: []std.Thread, - -const RunQueue = std.SinglyLinkedList(Runnable); -const Runnable = struct { - runFn: RunProto, -}; - -const RunProto = *const fn (*Runnable) void; - -pub fn init(pool: *ThreadPool, allocator: std.mem.Allocator) !void { - pool.* = .{ - .allocator = allocator, - .threads = &[_]std.Thread{}, - }; - - if (builtin.single_threaded) { - return; - } - - const thread_count = std.math.max(1, std.Thread.getCpuCount() catch 1); - pool.threads = try allocator.alloc(std.Thread, thread_count); - errdefer allocator.free(pool.threads); - - // kill and join any threads we spawned previously on error. - var spawned: usize = 0; - errdefer pool.join(spawned); - - for (pool.threads) |*thread| { - thread.* = try std.Thread.spawn(.{}, worker, .{pool}); - spawned += 1; - } -} - -pub fn deinit(pool: *ThreadPool) void { - pool.join(pool.threads.len); // kill and join all threads. - pool.* = undefined; -} - -fn join(pool: *ThreadPool, spawned: usize) void { - if (builtin.single_threaded) { - return; - } - - { - pool.mutex.lock(); - defer pool.mutex.unlock(); - - // ensure future worker threads exit the dequeue loop - pool.is_running = false; - } - - // wake up any sleeping threads (this can be done outside the mutex) - // then wait for all the threads we know are spawned to complete. - pool.cond.broadcast(); - for (pool.threads[0..spawned]) |thread| { - thread.join(); - } - - pool.allocator.free(pool.threads); -} - -pub fn spawn(pool: *ThreadPool, comptime func: anytype, args: anytype) !void { - if (builtin.single_threaded) { - @call(.auto, func, args); - return; - } - - const Args = @TypeOf(args); - const Closure = struct { - arguments: Args, - pool: *ThreadPool, - run_node: RunQueue.Node = .{ .data = .{ .runFn = runFn } }, - - fn runFn(runnable: *Runnable) void { - const run_node = @fieldParentPtr(RunQueue.Node, "data", runnable); - const closure = @fieldParentPtr(@This(), "run_node", run_node); - @call(.auto, func, closure.arguments); - - // The thread pool's allocator is protected by the mutex. - const mutex = &closure.pool.mutex; - mutex.lock(); - defer mutex.unlock(); - - closure.pool.allocator.destroy(closure); - } - }; - - { - pool.mutex.lock(); - defer pool.mutex.unlock(); - - const closure = try pool.allocator.create(Closure); - closure.* = .{ - .arguments = args, - .pool = pool, - }; - - pool.run_queue.prepend(&closure.run_node); - } - - // Notify waiting threads outside the lock to try and keep the critical section small. - pool.cond.signal(); -} - -fn worker(pool: *ThreadPool) void { - pool.mutex.lock(); - defer pool.mutex.unlock(); - - while (true) { - while (pool.run_queue.popFirst()) |run_node| { - // Temporarily unlock the mutex in order to execute the run_node - pool.mutex.unlock(); - defer pool.mutex.lock(); - - const runFn = run_node.data.runFn; - runFn(&run_node.data); - } - - // Stop executing instead of waiting if the thread pool is no longer running. - if (pool.is_running) { - pool.cond.wait(&pool.mutex); - } else { - break; - } - } -} - -pub fn waitAndWork(pool: *ThreadPool, wait_group: *WaitGroup) void { - while (!wait_group.isDone()) { - if (blk: { - pool.mutex.lock(); - defer pool.mutex.unlock(); - break :blk pool.run_queue.popFirst(); - }) |run_node| { - run_node.data.runFn(&run_node.data); - continue; - } - - wait_group.wait(); - return; - } -} diff --git a/src/WaitGroup.zig b/src/WaitGroup.zig deleted file mode 100644 index c8be6658db76bc530f35bda4785274a3882bb319..0000000000000000000000000000000000000000 --- a/src/WaitGroup.zig +++ /dev/null @@ -1,46 +0,0 @@ -const std = @import("std"); -const Atomic = std.atomic.Atomic; -const assert = std.debug.assert; -const WaitGroup = @This(); - -const is_waiting: usize = 1 << 0; -const one_pending: usize = 1 << 1; - -state: Atomic(usize) = Atomic(usize).init(0), -event: std.Thread.ResetEvent = .{}, - -pub fn start(self: *WaitGroup) void { - const state = self.state.fetchAdd(one_pending, .Monotonic); - assert((state / one_pending) < (std.math.maxInt(usize) / one_pending)); -} - -pub fn finish(self: *WaitGroup) void { - const state = self.state.fetchSub(one_pending, .Release); - assert((state / one_pending) > 0); - - if (state == (one_pending | is_waiting)) { - self.state.fence(.Acquire); - self.event.set(); - } -} - -pub fn wait(self: *WaitGroup) void { - var state = self.state.fetchAdd(is_waiting, .Acquire); - assert(state & is_waiting == 0); - - if ((state / one_pending) > 0) { - self.event.wait(); - } -} - -pub fn reset(self: *WaitGroup) void { - self.state.store(0, .Monotonic); - self.event.reset(); -} - -pub fn isDone(wg: *WaitGroup) bool { - const state = wg.state.load(.Acquire); - assert(state & is_waiting == 0); - - return (state / one_pending) == 0; -} diff --git a/src/link/MachO/CodeSignature.zig b/src/link/MachO/CodeSignature.zig index 8bc00d9181a5152b0a9046ecffb7f781f0e4aeaf..6d1cd7b53676672640e1303c800d93348c903615 100644 --- a/src/link/MachO/CodeSignature.zig +++ b/src/link/MachO/CodeSignature.zig @@ -7,12 +7,12 @@ const log = std.log.scoped(.link); const macho = std.macho; const mem = std.mem; const testing = std.testing; +const ThreadPool = std.Thread.Pool; +const WaitGroup = std.Thread.WaitGroup; const Allocator = mem.Allocator; const Compilation = @import("../../Compilation.zig"); const Sha256 = std.crypto.hash.sha2.Sha256; -const ThreadPool = @import("../../ThreadPool.zig"); -const WaitGroup = @import("../../WaitGroup.zig"); const hash_size = Sha256.digest_length; diff --git a/src/main.zig b/src/main.zig index 95cfca1463cdf9720caa3a9056e23ef590938039..dd0faa628c5fd0aaaab412ac3332466c3bac50ef 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ const Allocator = mem.Allocator; const ArrayList = std.ArrayList; const Ast = std.zig.Ast; const warn = std.log.warn; +const ThreadPool = std.Thread.Pool; const tracy = @import("tracy.zig"); const Compilation = @import("Compilation.zig"); @@ -22,7 +23,6 @@ const translate_c = @import("translate_c.zig"); const clang = @import("clang.zig"); const Cache = std.Build.Cache; const target_util = @import("target.zig"); -const ThreadPool = @import("ThreadPool.zig"); const crash_report = @import("crash_report.zig"); pub const std_options = struct { diff --git a/src/test.zig b/src/test.zig index 61cdb705e3a24c4bd73831a1f4c49aa40889cabe..ce8774260609a160565b6339c7b9dd24b5a48588 100644 --- a/src/test.zig +++ b/src/test.zig @@ -4,14 +4,14 @@ const Allocator = std.mem.Allocator; const CrossTarget = std.zig.CrossTarget; const print = std.debug.print; const assert = std.debug.assert; +const ThreadPool = std.Thread.Pool; +const WaitGroup = std.Thread.WaitGroup; const link = @import("link.zig"); const Compilation = @import("Compilation.zig"); const Package = @import("Package.zig"); const introspect = @import("introspect.zig"); const build_options = @import("build_options"); -const ThreadPool = @import("ThreadPool.zig"); -const WaitGroup = @import("WaitGroup.zig"); const zig_h = link.File.C.zig_h; const enable_qemu: bool = build_options.enable_qemu;