| author | |
| committer | |
| log | 3b4432d9a6ae7f01f66e5ddcc15bf579d2c47460 |
| tree | d24b87f33bf868ad6b4857bb500a80441744cc4a |
| parent | 2f52f95b928c1dcb42afb22243c27fb9661cbd1b |
| parent | 12508025a4b3a841819b913e4ed88810ca04ba79 |
| signature |
Integrate std.time.sleep with the event loop4 files changed, 391 insertions(+), 1 deletions(-)
lib/std/auto_reset_event.zig created+229| ... | ... | @@ -0,0 +1,229 @@ |
| 1 | // SPDX-License-Identifier: MIT | |
| 2 | // Copyright (c) 2015-2020 Zig Contributors | |
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. | |
| 4 | // The MIT license requires this copyright notice to be included in all copies | |
| 5 | // and substantial portions of the software. | |
| 6 | const std = @import("std.zig"); | |
| 7 | const builtin = @import("builtin"); | |
| 8 | const testing = std.testing; | |
| 9 | const assert = std.debug.assert; | |
| 10 | ||
| 11 | /// Similar to std.ResetEvent but on `set()` it also (atomically) does `reset()`. | |
| 12 | /// Unlike std.ResetEvent, `wait()` can only be called by one thread (MPSC-like). | |
| 13 | pub const AutoResetEvent = struct { | |
| 14 | // AutoResetEvent has 3 possible states: | |
| 15 | // - UNSET: the AutoResetEvent is currently unset | |
| 16 | // - SET: the AutoResetEvent was notified before a wait() was called | |
| 17 | // - <std.ResetEvent pointer>: there is an active waiter waiting for a notification. | |
| 18 | // | |
| 19 | // When attempting to wait: | |
| 20 | // if the event is unset, it registers a ResetEvent pointer to be notified when the event is set | |
| 21 | // if the event is already set, then it consumes the notification and resets the event. | |
| 22 | // | |
| 23 | // When attempting to notify: | |
| 24 | // if the event is unset, then we set the event | |
| 25 | // if theres a waiting ResetEvent, then we unset the event and notify the ResetEvent | |
| 26 | // | |
| 27 | // This ensures that the event is automatically reset after a wait() has been issued | |
| 28 | // and avoids the race condition when using std.ResetEvent in the following scenario: | |
| 29 | // thread 1 | thread 2 | |
| 30 | // std.ResetEvent.wait() | | |
| 31 | // | std.ResetEvent.set() | |
| 32 | // | std.ResetEvent.set() | |
| 33 | // std.ResetEvent.reset() | | |
| 34 | // std.ResetEvent.wait() | (missed the second .set() notification above) | |
| 35 | ||
| 36 | ||
| 37 | state: usize = UNSET, | |
| 38 | ||
| 39 | const UNSET = 0; | |
| 40 | const SET = 1; | |
| 41 | ||
| 42 | // the minimum alignment for the `*std.ResetEvent` created by wait*() | |
| 43 | const event_align = std.math.max(@alignOf(std.ResetEvent), 2); | |
| 44 | ||
| 45 | pub fn wait(self: *AutoResetEvent) void { | |
| 46 | self.waitFor(null) catch unreachable; | |
| 47 | } | |
| 48 | ||
| 49 | pub fn timedWait(self: *AutoResetEvent, timeout: u64) error{TimedOut}!void { | |
| 50 | return self.waitFor(timeout); | |
| 51 | } | |
| 52 | ||
| 53 | fn waitFor(self: *AutoResetEvent, timeout: ?u64) error{TimedOut}!void { | |
| 54 | // lazily initialized std.ResetEvent | |
| 55 | var reset_event: std.ResetEvent align(event_align) = undefined; | |
| 56 | var has_reset_event = false; | |
| 57 | defer if (has_reset_event) { | |
| 58 | reset_event.deinit(); | |
| 59 | }; | |
| 60 | ||
| 61 | var state = @atomicLoad(usize, &self.state, .SeqCst); | |
| 62 | while (true) { | |
| 63 | // consume a notification if there is any | |
| 64 | if (state == SET) { | |
| 65 | @atomicStore(usize, &self.state, UNSET, .SeqCst); | |
| 66 | return; | |
| 67 | } | |
| 68 | ||
| 69 | // check if theres currently a pending ResetEvent pointer already registered | |
| 70 | if (state != UNSET) { | |
| 71 | unreachable; // multiple waiting threads on the same AutoResetEvent | |
| 72 | } | |
| 73 | ||
| 74 | // lazily initialize the ResetEvent if it hasn't been already | |
| 75 | if (!has_reset_event) { | |
| 76 | has_reset_event = true; | |
| 77 | reset_event = std.ResetEvent.init(); | |
| 78 | } | |
| 79 | ||
| 80 | // Since the AutoResetEvent currently isnt set, | |
| 81 | // try to register our ResetEvent on it to wait | |
| 82 | // for a set() call from another thread. | |
| 83 | if (@cmpxchgWeak( | |
| 84 | usize, | |
| 85 | &self.state, | |
| 86 | UNSET, | |
| 87 | @ptrToInt(&reset_event), | |
| 88 | .SeqCst, | |
| 89 | .SeqCst, | |
| 90 | )) |new_state| { | |
| 91 | state = new_state; | |
| 92 | continue; | |
| 93 | } | |
| 94 | ||
| 95 | // if no timeout was specified, then just wait forever | |
| 96 | const timeout_ns = timeout orelse { | |
| 97 | reset_event.wait(); | |
| 98 | return; | |
| 99 | }; | |
| 100 | ||
| 101 | // wait with a timeout and return if signalled via set() | |
| 102 | if (reset_event.timedWait(timeout_ns)) |_| { | |
| 103 | return; | |
| 104 | } else |timed_out| {} | |
| 105 | ||
| 106 | // If we timed out, we need to transition the AutoResetEvent back to UNSET. | |
| 107 | // If we don't, then when we return, a set() thread could observe a pointer to an invalid ResetEvent. | |
| 108 | state = @cmpxchgStrong( | |
| 109 | usize, | |
| 110 | &self.state, | |
| 111 | @ptrToInt(&reset_event), | |
| 112 | UNSET, | |
| 113 | .SeqCst, | |
| 114 | .SeqCst, | |
| 115 | ) orelse return error.TimedOut; | |
| 116 | ||
| 117 | // We didn't manage to unregister ourselves from the state. | |
| 118 | if (state == SET) { | |
| 119 | unreachable; // AutoResetEvent notified without waking up the waiting thread | |
| 120 | } else if (state != UNSET) { | |
| 121 | unreachable; // multiple waiting threads on the same AutoResetEvent observed when timing out | |
| 122 | } | |
| 123 | ||
| 124 | // This menas a set() thread saw our ResetEvent pointer, acquired it, and is trying to wake it up. | |
| 125 | // We need to wait for it to wake up our ResetEvent before we can return and invalidate it. | |
| 126 | // We don't return error.TimedOut here as it technically notified us while we were "timing out". | |
| 127 | reset_event.wait(); | |
| 128 | return; | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | pub fn set(self: *AutoResetEvent) void { | |
| 133 | var state = @atomicLoad(usize, &self.state, .SeqCst); | |
| 134 | while (true) { | |
| 135 | // If the AutoResetEvent is already set, there is nothing else left to do | |
| 136 | if (state == SET) { | |
| 137 | return; | |
| 138 | } | |
| 139 | ||
| 140 | // If the AutoResetEvent isn't set, | |
| 141 | // then try to leave a notification for the wait() thread that we set() it. | |
| 142 | if (state == UNSET) { | |
| 143 | state = @cmpxchgWeak( | |
| 144 | usize, | |
| 145 | &self.state, | |
| 146 | UNSET, | |
| 147 | SET, | |
| 148 | .SeqCst, | |
| 149 | .SeqCst, | |
| 150 | ) orelse return; | |
| 151 | continue; | |
| 152 | } | |
| 153 | ||
| 154 | // There is a ResetEvent pointer registered on the AutoResetEvent event thats waiting. | |
| 155 | // Try to acquire ownership of it so that we can wake it up. | |
| 156 | // This also resets the AutoResetEvent so that there is no race condition as defined above. | |
| 157 | if (@cmpxchgWeak( | |
| 158 | usize, | |
| 159 | &self.state, | |
| 160 | state, | |
| 161 | UNSET, | |
| 162 | .SeqCst, | |
| 163 | .SeqCst, | |
| 164 | )) |new_state| { | |
| 165 | state = new_state; | |
| 166 | continue; | |
| 167 | } | |
| 168 | ||
| 169 | const reset_event = @intToPtr(*align(event_align) std.ResetEvent, state); | |
| 170 | reset_event.set(); | |
| 171 | return; | |
| 172 | } | |
| 173 | } | |
| 174 | }; | |
| 175 | ||
| 176 | test "std.AutoResetEvent" { | |
| 177 | // test local code paths | |
| 178 | { | |
| 179 | var event = AutoResetEvent{}; | |
| 180 | testing.expectError(error.TimedOut, event.timedWait(1)); | |
| 181 | event.set(); | |
| 182 | event.wait(); | |
| 183 | } | |
| 184 | ||
| 185 | // test cross-thread signaling | |
| 186 | if (builtin.single_threaded) | |
| 187 | return; | |
| 188 | ||
| 189 | const Context = struct { | |
| 190 | value: u128 = 0, | |
| 191 | in: AutoResetEvent = AutoResetEvent{}, | |
| 192 | out: AutoResetEvent = AutoResetEvent{}, | |
| 193 | ||
| 194 | const Self = @This(); | |
| 195 | ||
| 196 | fn sender(self: *Self) void { | |
| 197 | testing.expect(self.value == 0); | |
| 198 | self.value = 1; | |
| 199 | self.out.set(); | |
| 200 | ||
| 201 | self.in.wait(); | |
| 202 | testing.expect(self.value == 2); | |
| 203 | self.value = 3; | |
| 204 | self.out.set(); | |
| 205 | ||
| 206 | self.in.wait(); | |
| 207 | testing.expect(self.value == 4); | |
| 208 | } | |
| 209 | ||
| 210 | fn receiver(self: *Self) void { | |
| 211 | self.out.wait(); | |
| 212 | testing.expect(self.value == 1); | |
| 213 | self.value = 2; | |
| 214 | self.in.set(); | |
| 215 | ||
| 216 | self.out.wait(); | |
| 217 | testing.expect(self.value == 3); | |
| 218 | self.value = 4; | |
| 219 | self.in.set(); | |
| 220 | } | |
| 221 | }; | |
| 222 | ||
| 223 | var context = Context{}; | |
| 224 | const send_thread = try std.Thread.spawn(&context, Context.sender); | |
| 225 | const recv_thread = try std.Thread.spawn(&context, Context.receiver); | |
| 226 | ||
| 227 | send_thread.wait(); | |
| 228 | recv_thread.wait(); | |
| 229 | } | |
| \ No newline at end of file |
lib/std/event/loop.zig+157| ... | ... | @@ -35,6 +35,9 @@ pub const Loop = struct { |
| 35 | 35 | /// This is only used by `Loop` for the thread pool and associated resources. |
| 36 | 36 | arena: std.heap.ArenaAllocator, |
| 37 | 37 | |
| 38 | /// State which manages frames that are sleeping on timers | |
| 39 | delay_queue: DelayQueue, | |
| 40 | ||
| 38 | 41 | /// Pre-allocated eventfds. All permanently active. |
| 39 | 42 | /// This is how `Loop` sends promises to be resumed on other threads. |
| 40 | 43 | available_eventfd_resume_nodes: std.atomic.Stack(ResumeNode.EventFd), |
| ... | ... | @@ -162,6 +165,7 @@ pub const Loop = struct { |
| 162 | 165 | .fs_queue = std.atomic.Queue(Request).init(), |
| 163 | 166 | .fs_thread = undefined, |
| 164 | 167 | .fs_thread_wakeup = std.ResetEvent.init(), |
| 168 | .delay_queue = undefined, | |
| 165 | 169 | }; |
| 166 | 170 | errdefer self.fs_thread_wakeup.deinit(); |
| 167 | 171 | errdefer self.arena.deinit(); |
| ... | ... | @@ -186,6 +190,9 @@ pub const Loop = struct { |
| 186 | 190 | self.posixFsRequest(&self.fs_end_request); |
| 187 | 191 | self.fs_thread.wait(); |
| 188 | 192 | }; |
| 193 | ||
| 194 | if (!std.builtin.single_threaded) | |
| 195 | try self.delay_queue.init(); | |
| 189 | 196 | } |
| 190 | 197 | |
| 191 | 198 | pub fn deinit(self: *Loop) void { |
| ... | ... | @@ -645,6 +652,10 @@ pub const Loop = struct { |
| 645 | 652 | for (self.extra_threads) |extra_thread| { |
| 646 | 653 | extra_thread.wait(); |
| 647 | 654 | } |
| 655 | ||
| 656 | @atomicStore(bool, &self.delay_queue.is_running, false, .SeqCst); | |
| 657 | self.delay_queue.event.set(); | |
| 658 | self.delay_queue.thread.wait(); | |
| 648 | 659 | } |
| 649 | 660 | |
| 650 | 661 | /// Runs the provided function asynchronously. The function's frame is allocated |
| ... | ... | @@ -748,6 +759,128 @@ pub const Loop = struct { |
| 748 | 759 | } |
| 749 | 760 | } |
| 750 | 761 | |
| 762 | pub fn sleep(self: *Loop, nanoseconds: u64) void { | |
| 763 | if (std.builtin.single_threaded) | |
| 764 | @compileError("TODO: integrate timers with epoll/kevent/iocp for single-threaded"); | |
| 765 | ||
| 766 | suspend { | |
| 767 | const now = self.delay_queue.timer.read(); | |
| 768 | ||
| 769 | var entry: DelayQueue.Waiters.Entry = undefined; | |
| 770 | entry.init(@frame(), now + nanoseconds); | |
| 771 | self.delay_queue.waiters.insert(&entry); | |
| 772 | ||
| 773 | // Speculatively wake up the timer thread when we add a new entry. | |
| 774 | // If the timer thread is sleeping on a longer entry, we need to | |
| 775 | // interrupt it so that our entry can be expired in time. | |
| 776 | self.delay_queue.event.set(); | |
| 777 | } | |
| 778 | } | |
| 779 | ||
| 780 | const DelayQueue = struct { | |
| 781 | timer: std.time.Timer, | |
| 782 | waiters: Waiters, | |
| 783 | thread: *std.Thread, | |
| 784 | event: std.AutoResetEvent, | |
| 785 | is_running: bool, | |
| 786 | ||
| 787 | /// Initialize the delay queue by spawning the timer thread | |
| 788 | /// and starting any timer resources. | |
| 789 | fn init(self: *DelayQueue) !void { | |
| 790 | self.* = DelayQueue{ | |
| 791 | .timer = try std.time.Timer.start(), | |
| 792 | .waiters = DelayQueue.Waiters{ | |
| 793 | .entries = std.atomic.Queue(anyframe).init(), | |
| 794 | }, | |
| 795 | .thread = try std.Thread.spawn(self, DelayQueue.run), | |
| 796 | .event = std.AutoResetEvent{}, | |
| 797 | .is_running = true, | |
| 798 | }; | |
| 799 | } | |
| 800 | ||
| 801 | /// Entry point for the timer thread | |
| 802 | /// which waits for timer entries to expire and reschedules them. | |
| 803 | fn run(self: *DelayQueue) void { | |
| 804 | const loop = @fieldParentPtr(Loop, "delay_queue", self); | |
| 805 | ||
| 806 | while (@atomicLoad(bool, &self.is_running, .SeqCst)) { | |
| 807 | const now = self.timer.read(); | |
| 808 | ||
| 809 | if (self.waiters.popExpired(now)) |entry| { | |
| 810 | loop.onNextTick(&entry.node); | |
| 811 | continue; | |
| 812 | } | |
| 813 | ||
| 814 | if (self.waiters.nextExpire()) |expires| { | |
| 815 | if (now >= expires) | |
| 816 | continue; | |
| 817 | self.event.timedWait(expires - now) catch {}; | |
| 818 | } else { | |
| 819 | self.event.wait(); | |
| 820 | } | |
| 821 | } | |
| 822 | } | |
| 823 | ||
| 824 | // TODO: use a tickless heirarchical timer wheel: | |
| 825 | // https://github.com/wahern/timeout/ | |
| 826 | const Waiters = struct { | |
| 827 | entries: std.atomic.Queue(anyframe), | |
| 828 | ||
| 829 | const Entry = struct { | |
| 830 | node: NextTickNode, | |
| 831 | expires: u64, | |
| 832 | ||
| 833 | fn init(self: *Entry, frame: anyframe, expires: u64) void { | |
| 834 | self.node.data = frame; | |
| 835 | self.expires = expires; | |
| 836 | } | |
| 837 | }; | |
| 838 | ||
| 839 | /// Registers the entry into the queue of waiting frames | |
| 840 | fn insert(self: *Waiters, entry: *Entry) void { | |
| 841 | self.entries.put(&entry.node); | |
| 842 | } | |
| 843 | ||
| 844 | /// Dequeues one expired event relative to `now` | |
| 845 | fn popExpired(self: *Waiters, now: u64) ?*Entry { | |
| 846 | const entry = self.peekExpiringEntry() orelse return null; | |
| 847 | if (entry.expires > now) | |
| 848 | return null; | |
| 849 | ||
| 850 | assert(self.entries.remove(&entry.node)); | |
| 851 | return entry; | |
| 852 | } | |
| 853 | ||
| 854 | /// Returns an estimate for the amount of time | |
| 855 | /// to wait until the next waiting entry expires. | |
| 856 | fn nextExpire(self: *Waiters) ?u64 { | |
| 857 | const entry = self.peekExpiringEntry() orelse return null; | |
| 858 | return entry.expires; | |
| 859 | } | |
| 860 | ||
| 861 | fn peekExpiringEntry(self: *Waiters) ?*Entry { | |
| 862 | const held = self.entries.mutex.acquire(); | |
| 863 | defer held.release(); | |
| 864 | ||
| 865 | // starting from the head | |
| 866 | var head = self.entries.head orelse return null; | |
| 867 | ||
| 868 | // traverse the list of waiting entires to | |
| 869 | // find the Node with the smallest `expires` field | |
| 870 | var min = head; | |
| 871 | while (head.next) |node| { | |
| 872 | const minEntry = @fieldParentPtr(Entry, "node", min); | |
| 873 | const nodeEntry = @fieldParentPtr(Entry, "node", node); | |
| 874 | if (nodeEntry.expires < minEntry.expires) | |
| 875 | min = node; | |
| 876 | head = node; | |
| 877 | } | |
| 878 | ||
| 879 | return @fieldParentPtr(Entry, "node", min); | |
| 880 | } | |
| 881 | }; | |
| 882 | }; | |
| 883 | ||
| 751 | 884 | /// ------- I/0 APIs ------- |
| 752 | 885 | pub fn accept( |
| 753 | 886 | self: *Loop, |
| ... | ... | @@ -1550,3 +1683,27 @@ test "std.event.Loop - runDetached" { |
| 1550 | 1683 | fn testRunDetached() void { |
| 1551 | 1684 | testRunDetachedData += 1; |
| 1552 | 1685 | } |
| 1686 | ||
| 1687 | test "std.event.Loop - sleep" { | |
| 1688 | // https://github.com/ziglang/zig/issues/1908 | |
| 1689 | if (builtin.single_threaded) return error.SkipZigTest; | |
| 1690 | if (!std.io.is_async) return error.SkipZigTest; | |
| 1691 | ||
| 1692 | const frames = try testing.allocator.alloc(@Frame(testSleep), 10); | |
| 1693 | defer testing.allocator.free(frames); | |
| 1694 | ||
| 1695 | const wait_time = 100 * std.time.ns_per_ms; | |
| 1696 | var sleep_count: usize = 0; | |
| 1697 | ||
| 1698 | for (frames) |*frame| | |
| 1699 | frame.* = async testSleep(wait_time, &sleep_count); | |
| 1700 | for (frames) |*frame| | |
| 1701 | await frame; | |
| 1702 | ||
| 1703 | testing.expect(sleep_count == frames.len); | |
| 1704 | } | |
| 1705 | ||
| 1706 | fn testSleep(wait_ns: u64, sleep_count: *usize) void { | |
| 1707 | Loop.instance.?.sleep(wait_ns); | |
| 1708 | _ = @atomicRmw(usize, sleep_count, .Add, 1, .SeqCst); | |
| 1709 | } |
lib/std/std.zig+1| ... | ... | @@ -14,6 +14,7 @@ pub const AutoArrayHashMap = array_hash_map.AutoArrayHashMap; |
| 14 | 14 | pub const AutoArrayHashMapUnmanaged = array_hash_map.AutoArrayHashMapUnmanaged; |
| 15 | 15 | pub const AutoHashMap = hash_map.AutoHashMap; |
| 16 | 16 | pub const AutoHashMapUnmanaged = hash_map.AutoHashMapUnmanaged; |
| 17 | pub const AutoResetEvent = @import("auto_reset_event.zig").AutoResetEvent; | |
| 17 | 18 | pub const BufMap = @import("buf_map.zig").BufMap; |
| 18 | 19 | pub const BufSet = @import("buf_set.zig").BufSet; |
| 19 | 20 | pub const ChildProcess = @import("child_process.zig").ChildProcess; |
lib/std/time.zig+4-1| ... | ... | @@ -14,8 +14,11 @@ const is_windows = std.Target.current.os.tag == .windows; |
| 14 | 14 | pub const epoch = @import("time/epoch.zig"); |
| 15 | 15 | |
| 16 | 16 | /// Spurious wakeups are possible and no precision of timing is guaranteed. |
| 17 | /// TODO integrate with evented I/O | |
| 18 | 17 | pub fn sleep(nanoseconds: u64) void { |
| 18 | // TODO: opting out of async sleeping? | |
| 19 | if (std.io.is_async) | |
| 20 | return std.event.Loop.instance.?.sleep(nanoseconds); | |
| 21 | ||
| 19 | 22 | if (is_windows) { |
| 20 | 23 | const big_ms_from_ns = nanoseconds / ns_per_ms; |
| 21 | 24 | const ms = math.cast(os.windows.DWORD, big_ms_from_ns) catch math.maxInt(os.windows.DWORD); |