| author | |
| committer | |
| log | 5508b4c8876074b736ed436f0d20d5cec86f1f85 |
| tree | e2bddde9b476d377b8035b643571e83927de276d |
| parent | b01244d225eb35eac9e06f0677e6b3fc212b4b26 |
3 files changed, 497 insertions(+), 23 deletions(-)
lib/std/Io.zig+311-8| ... | @@ -6,6 +6,7 @@ const windows = std.os.windows; | ... | @@ -6,6 +6,7 @@ const windows = std.os.windows; |
| 6 | const posix = std.posix; | 6 | const posix = std.posix; |
| 7 | const math = std.math; | 7 | const math = std.math; |
| 8 | const assert = std.debug.assert; | 8 | const assert = std.debug.assert; |
| 9 | const fs = std.fs; | ||
| 9 | const Allocator = std.mem.Allocator; | 10 | const Allocator = std.mem.Allocator; |
| 10 | const Alignment = std.mem.Alignment; | 11 | const Alignment = std.mem.Alignment; |
| 11 | 12 | ||
| ... | @@ -614,6 +615,12 @@ pub const VTable = struct { | ... | @@ -614,6 +615,12 @@ pub const VTable = struct { |
| 614 | /// Thread-safe. | 615 | /// Thread-safe. |
| 615 | cancelRequested: *const fn (?*anyopaque) bool, | 616 | cancelRequested: *const fn (?*anyopaque) bool, |
| 616 | 617 | ||
| 618 | mutexLock: *const fn (?*anyopaque, mutex: *Mutex) void, | ||
| 619 | mutexUnlock: *const fn (?*anyopaque, mutex: *Mutex) void, | ||
| 620 | |||
| 621 | conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex, timeout_ns: ?u64) Condition.WaitError!void, | ||
| 622 | conditionWake: *const fn (?*anyopaque, cond: *Condition, notify: Condition.Notify) void, | ||
| 623 | |||
| 617 | createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, | 624 | createFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.CreateFlags) FileOpenError!fs.File, |
| 618 | openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, | 625 | openFile: *const fn (?*anyopaque, dir: fs.Dir, sub_path: []const u8, flags: fs.File.OpenFlags) FileOpenError!fs.File, |
| 619 | closeFile: *const fn (?*anyopaque, fs.File) void, | 626 | closeFile: *const fn (?*anyopaque, fs.File) void, |
| ... | @@ -627,11 +634,11 @@ pub const VTable = struct { | ... | @@ -627,11 +634,11 @@ pub const VTable = struct { |
| 627 | pub const OpenFlags = fs.File.OpenFlags; | 634 | pub const OpenFlags = fs.File.OpenFlags; |
| 628 | pub const CreateFlags = fs.File.CreateFlags; | 635 | pub const CreateFlags = fs.File.CreateFlags; |
| 629 | 636 | ||
| 630 | pub const FileOpenError = fs.File.OpenError || error{AsyncCancel}; | 637 | pub const FileOpenError = fs.File.OpenError || error{Canceled}; |
| 631 | pub const FileReadError = fs.File.ReadError || error{AsyncCancel}; | 638 | pub const FileReadError = fs.File.ReadError || error{Canceled}; |
| 632 | pub const FilePReadError = fs.File.PReadError || error{AsyncCancel}; | 639 | pub const FilePReadError = fs.File.PReadError || error{Canceled}; |
| 633 | pub const FileWriteError = fs.File.WriteError || error{AsyncCancel}; | 640 | pub const FileWriteError = fs.File.WriteError || error{Canceled}; |
| 634 | pub const FilePWriteError = fs.File.PWriteError || error{AsyncCancel}; | 641 | pub const FilePWriteError = fs.File.PWriteError || error{Canceled}; |
| 635 | 642 | ||
| 636 | pub const Timestamp = enum(i96) { | 643 | pub const Timestamp = enum(i96) { |
| 637 | _, | 644 | _, |
| ... | @@ -648,8 +655,8 @@ pub const Deadline = union(enum) { | ... | @@ -648,8 +655,8 @@ pub const Deadline = union(enum) { |
| 648 | nanoseconds: i96, | 655 | nanoseconds: i96, |
| 649 | timestamp: Timestamp, | 656 | timestamp: Timestamp, |
| 650 | }; | 657 | }; |
| 651 | pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{AsyncCancel}; | 658 | pub const ClockGetTimeError = std.posix.ClockGetTimeError || error{Canceled}; |
| 652 | pub const SleepError = error{ UnsupportedClock, Unexpected, AsyncCancel }; | 659 | pub const SleepError = error{ UnsupportedClock, Unexpected, Canceled }; |
| 653 | 660 | ||
| 654 | pub const AnyFuture = opaque {}; | 661 | pub const AnyFuture = opaque {}; |
| 655 | 662 | ||
| ... | @@ -678,6 +685,302 @@ pub fn Future(Result: type) type { | ... | @@ -678,6 +685,302 @@ pub fn Future(Result: type) type { |
| 678 | }; | 685 | }; |
| 679 | } | 686 | } |
| 680 | 687 | ||
| 688 | pub const Mutex = struct { | ||
| 689 | state: std.atomic.Value(u32) = std.atomic.Value(u32).init(unlocked), | ||
| 690 | |||
| 691 | pub const unlocked: u32 = 0b00; | ||
| 692 | pub const locked: u32 = 0b01; | ||
| 693 | pub const contended: u32 = 0b11; // must contain the `locked` bit for x86 optimization below | ||
| 694 | |||
| 695 | pub fn tryLock(m: *Mutex) bool { | ||
| 696 | // On x86, use `lock bts` instead of `lock cmpxchg` as: | ||
| 697 | // - they both seem to mark the cache-line as modified regardless: https://stackoverflow.com/a/63350048 | ||
| 698 | // - `lock bts` is smaller instruction-wise which makes it better for inlining | ||
| 699 | if (builtin.target.cpu.arch.isX86()) { | ||
| 700 | const locked_bit = @ctz(locked); | ||
| 701 | return m.state.bitSet(locked_bit, .acquire) == 0; | ||
| 702 | } | ||
| 703 | |||
| 704 | // Acquire barrier ensures grabbing the lock happens before the critical section | ||
| 705 | // and that the previous lock holder's critical section happens before we grab the lock. | ||
| 706 | return m.state.cmpxchgWeak(unlocked, locked, .acquire, .monotonic) == null; | ||
| 707 | } | ||
| 708 | |||
| 709 | /// Avoids the vtable for uncontended locks. | ||
| 710 | pub fn lock(m: *Mutex, io: Io) void { | ||
| 711 | if (!m.tryLock()) { | ||
| 712 | @branchHint(.unlikely); | ||
| 713 | io.vtable.mutexLock(io.userdata, m); | ||
| 714 | } | ||
| 715 | } | ||
| 716 | |||
| 717 | pub fn unlock(m: *Mutex, io: Io) void { | ||
| 718 | io.vtable.mutexUnlock(io.userdata, m); | ||
| 719 | } | ||
| 720 | }; | ||
| 721 | |||
| 722 | pub const Condition = struct { | ||
| 723 | state: u64 = 0, | ||
| 724 | |||
| 725 | pub const WaitError = error{ | ||
| 726 | Timeout, | ||
| 727 | Canceled, | ||
| 728 | }; | ||
| 729 | |||
| 730 | /// How many waiters to wake up. | ||
| 731 | pub const Notify = enum { | ||
| 732 | one, | ||
| 733 | all, | ||
| 734 | }; | ||
| 735 | |||
| 736 | pub fn wait(cond: *Condition, io: Io, mutex: *Mutex) void { | ||
| 737 | io.vtable.conditionWait(io.userdata, cond, mutex, null) catch |err| switch (err) { | ||
| 738 | error.Timeout => unreachable, // no timeout provided so we shouldn't have timed-out | ||
| 739 | error.Canceled => return, // handled as spurious wakeup | ||
| 740 | }; | ||
| 741 | } | ||
| 742 | |||
| 743 | pub fn timedWait(cond: *Condition, io: Io, mutex: *Mutex, timeout_ns: u64) WaitError!void { | ||
| 744 | return io.vtable.conditionWait(io.userdata, cond, mutex, timeout_ns); | ||
| 745 | } | ||
| 746 | |||
| 747 | pub fn signal(cond: *Condition, io: Io) void { | ||
| 748 | io.vtable.conditionWake(io.userdata, cond, .one); | ||
| 749 | } | ||
| 750 | |||
| 751 | pub fn broadcast(cond: *Condition, io: Io) void { | ||
| 752 | io.vtable.conditionWake(io.userdata, cond, .all); | ||
| 753 | } | ||
| 754 | }; | ||
| 755 | |||
| 756 | pub const TypeErasedQueue = struct { | ||
| 757 | mutex: Mutex, | ||
| 758 | |||
| 759 | /// Ring buffer. This data is logically *after* queued getters. | ||
| 760 | buffer: []u8, | ||
| 761 | put_index: usize, | ||
| 762 | get_index: usize, | ||
| 763 | |||
| 764 | putters: std.DoublyLinkedList(PutNode), | ||
| 765 | getters: std.DoublyLinkedList(GetNode), | ||
| 766 | |||
| 767 | const PutNode = struct { | ||
| 768 | remaining: []const u8, | ||
| 769 | condition: Condition, | ||
| 770 | }; | ||
| 771 | |||
| 772 | const GetNode = struct { | ||
| 773 | remaining: []u8, | ||
| 774 | condition: Condition, | ||
| 775 | }; | ||
| 776 | |||
| 777 | pub fn init(buffer: []u8) TypeErasedQueue { | ||
| 778 | return .{ | ||
| 779 | .mutex = .{}, | ||
| 780 | .buffer = buffer, | ||
| 781 | .put_index = 0, | ||
| 782 | .get_index = 0, | ||
| 783 | .putters = .{}, | ||
| 784 | .getters = .{}, | ||
| 785 | }; | ||
| 786 | } | ||
| 787 | |||
| 788 | pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize { | ||
| 789 | assert(elements.len >= min); | ||
| 790 | |||
| 791 | q.mutex.lock(io); | ||
| 792 | defer q.mutex.unlock(io); | ||
| 793 | |||
| 794 | // Getters have first priority on the data, and only when the getters | ||
| 795 | // queue is empty do we start populating the buffer. | ||
| 796 | |||
| 797 | var remaining = elements; | ||
| 798 | while (true) { | ||
| 799 | const getter = q.getters.popFirst() orelse break; | ||
| 800 | const copy_len = @min(getter.data.remaining.len, remaining.len); | ||
| 801 | @memcpy(getter.data.remaining[0..copy_len], remaining[0..copy_len]); | ||
| 802 | remaining = remaining[copy_len..]; | ||
| 803 | getter.data.remaining = getter.data.remaining[copy_len..]; | ||
| 804 | if (getter.data.remaining.len == 0) { | ||
| 805 | getter.data.condition.signal(io); | ||
| 806 | continue; | ||
| 807 | } | ||
| 808 | q.getters.prepend(getter); | ||
| 809 | assert(remaining.len == 0); | ||
| 810 | return elements.len; | ||
| 811 | } | ||
| 812 | |||
| 813 | while (true) { | ||
| 814 | { | ||
| 815 | const available = q.buffer[q.put_index..]; | ||
| 816 | const copy_len = @min(available.len, remaining.len); | ||
| 817 | @memcpy(available[0..copy_len], remaining[0..copy_len]); | ||
| 818 | remaining = remaining[copy_len..]; | ||
| 819 | q.put_index += copy_len; | ||
| 820 | if (remaining.len == 0) return elements.len; | ||
| 821 | } | ||
| 822 | { | ||
| 823 | const available = q.buffer[0..q.get_index]; | ||
| 824 | const copy_len = @min(available.len, remaining.len); | ||
| 825 | @memcpy(available[0..copy_len], remaining[0..copy_len]); | ||
| 826 | remaining = remaining[copy_len..]; | ||
| 827 | q.put_index = copy_len; | ||
| 828 | if (remaining.len == 0) return elements.len; | ||
| 829 | } | ||
| 830 | |||
| 831 | const total_filled = elements.len - remaining.len; | ||
| 832 | if (total_filled >= min) return total_filled; | ||
| 833 | |||
| 834 | var node: std.DoublyLinkedList(PutNode).Node = .{ | ||
| 835 | .data = .{ .remaining = remaining, .condition = .{} }, | ||
| 836 | }; | ||
| 837 | q.putters.append(&node); | ||
| 838 | node.data.condition.wait(io, &q.mutex); | ||
| 839 | remaining = node.data.remaining; | ||
| 840 | } | ||
| 841 | } | ||
| 842 | |||
| 843 | pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) usize { | ||
| 844 | assert(buffer.len >= min); | ||
| 845 | |||
| 846 | q.mutex.lock(io); | ||
| 847 | defer q.mutex.unlock(io); | ||
| 848 | |||
| 849 | // The ring buffer gets first priority, then data should come from any | ||
| 850 | // queued putters, then finally the ring buffer should be filled with | ||
| 851 | // data from putters so they can be resumed. | ||
| 852 | |||
| 853 | var remaining = buffer; | ||
| 854 | while (true) { | ||
| 855 | if (q.get_index <= q.put_index) { | ||
| 856 | const available = q.buffer[q.get_index..q.put_index]; | ||
| 857 | const copy_len = @min(available.len, remaining.len); | ||
| 858 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 859 | q.get_index += copy_len; | ||
| 860 | remaining = remaining[copy_len..]; | ||
| 861 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 862 | } else { | ||
| 863 | { | ||
| 864 | const available = q.buffer[q.get_index..]; | ||
| 865 | const copy_len = @min(available.len, remaining.len); | ||
| 866 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 867 | q.get_index += copy_len; | ||
| 868 | remaining = remaining[copy_len..]; | ||
| 869 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 870 | } | ||
| 871 | { | ||
| 872 | const available = q.buffer[0..q.put_index]; | ||
| 873 | const copy_len = @min(available.len, remaining.len); | ||
| 874 | @memcpy(remaining[0..copy_len], available[0..copy_len]); | ||
| 875 | q.get_index = copy_len; | ||
| 876 | remaining = remaining[copy_len..]; | ||
| 877 | if (remaining.len == 0) return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 878 | } | ||
| 879 | } | ||
| 880 | // Copy directly from putters into buffer. | ||
| 881 | while (remaining.len > 0) { | ||
| 882 | const putter = q.putters.popFirst() orelse break; | ||
| 883 | const copy_len = @min(putter.data.remaining.len, remaining.len); | ||
| 884 | @memcpy(remaining[0..copy_len], putter.data.remaining[0..copy_len]); | ||
| 885 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 886 | remaining = remaining[copy_len..]; | ||
| 887 | if (putter.data.remaining.len == 0) { | ||
| 888 | putter.data.condition.signal(io); | ||
| 889 | } else { | ||
| 890 | assert(remaining.len == 0); | ||
| 891 | q.putters.prepend(putter); | ||
| 892 | return fillRingBufferFromPutters(q, io, buffer.len); | ||
| 893 | } | ||
| 894 | } | ||
| 895 | // Both ring buffer and putters queue is empty. | ||
| 896 | const total_filled = buffer.len - remaining.len; | ||
| 897 | if (total_filled >= min) return total_filled; | ||
| 898 | |||
| 899 | var node: std.DoublyLinkedList(GetNode).Node = .{ | ||
| 900 | .data = .{ .remaining = remaining, .condition = .{} }, | ||
| 901 | }; | ||
| 902 | q.getters.append(&node); | ||
| 903 | node.data.condition.wait(io, &q.mutex); | ||
| 904 | remaining = node.data.remaining; | ||
| 905 | } | ||
| 906 | } | ||
| 907 | |||
| 908 | /// Called when there is nonzero space available in the ring buffer and | ||
| 909 | /// potentially putters waiting. The mutex is already held and the task is | ||
| 910 | /// to copy putter data to the ring buffer and signal any putters whose | ||
| 911 | /// buffers been fully copied. | ||
| 912 | fn fillRingBufferFromPutters(q: *TypeErasedQueue, io: Io, len: usize) usize { | ||
| 913 | while (true) { | ||
| 914 | const putter = q.putters.popFirst() orelse return len; | ||
| 915 | const available = q.buffer[q.put_index..]; | ||
| 916 | const copy_len = @min(available.len, putter.data.remaining.len); | ||
| 917 | @memcpy(available[0..copy_len], putter.data.remaining[0..copy_len]); | ||
| 918 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 919 | q.put_index += copy_len; | ||
| 920 | if (putter.data.remaining.len == 0) { | ||
| 921 | putter.data.condition.signal(io); | ||
| 922 | continue; | ||
| 923 | } | ||
| 924 | const second_available = q.buffer[0..q.get_index]; | ||
| 925 | const second_copy_len = @min(second_available.len, putter.data.remaining.len); | ||
| 926 | @memcpy(second_available[0..second_copy_len], putter.data.remaining[0..second_copy_len]); | ||
| 927 | putter.data.remaining = putter.data.remaining[copy_len..]; | ||
| 928 | q.put_index = copy_len; | ||
| 929 | if (putter.data.remaining.len == 0) { | ||
| 930 | putter.data.condition.signal(io); | ||
| 931 | continue; | ||
| 932 | } | ||
| 933 | q.putters.prepend(putter); | ||
| 934 | return len; | ||
| 935 | } | ||
| 936 | } | ||
| 937 | }; | ||
| 938 | |||
| 939 | /// Many producer, many consumer, thread-safe, runtime configurable buffer size. | ||
| 940 | /// When buffer is empty, consumers suspend and are resumed by producers. | ||
| 941 | /// When buffer is full, producers suspend and are resumed by consumers. | ||
| 942 | pub fn Queue(Elem: type) type { | ||
| 943 | return struct { | ||
| 944 | type_erased: TypeErasedQueue, | ||
| 945 | |||
| 946 | pub fn init(buffer: []Elem) @This() { | ||
| 947 | return .{ .type_erased = .init(@ptrCast(buffer)) }; | ||
| 948 | } | ||
| 949 | |||
| 950 | /// Appends elements to the end of the queue. The function returns when | ||
| 951 | /// at least `min` elements have been added to the buffer or sent | ||
| 952 | /// directly to a consumer. | ||
| 953 | /// | ||
| 954 | /// Returns how many elements have been added to the queue. | ||
| 955 | /// | ||
| 956 | /// Asserts that `elements.len >= min`. | ||
| 957 | pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) usize { | ||
| 958 | return @divExact(q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem)); | ||
| 959 | } | ||
| 960 | |||
| 961 | /// Receives elements from the beginning of the queue. The function | ||
| 962 | /// returns when at least `min` elements have been populated inside | ||
| 963 | /// `buffer`. | ||
| 964 | /// | ||
| 965 | /// Returns how many elements of `buffer` have been populated. | ||
| 966 | /// | ||
| 967 | /// Asserts that `buffer.len >= min`. | ||
| 968 | pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) usize { | ||
| 969 | return @divExact(q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem)); | ||
| 970 | } | ||
| 971 | |||
| 972 | pub fn putOne(q: *@This(), io: Io, item: Elem) void { | ||
| 973 | assert(q.put(io, &.{item}, 1) == 1); | ||
| 974 | } | ||
| 975 | |||
| 976 | pub fn getOne(q: *@This(), io: Io) Elem { | ||
| 977 | var buf: [1]Elem = undefined; | ||
| 978 | assert(q.get(io, &buf, 1) == 1); | ||
| 979 | return buf[0]; | ||
| 980 | } | ||
| 981 | }; | ||
| 982 | } | ||
| 983 | |||
| 681 | /// Calls `function` with `args`, such that the return value of the function is | 984 | /// Calls `function` with `args`, such that the return value of the function is |
| 682 | /// not guaranteed to be available until `await` is called. | 985 | /// not guaranteed to be available until `await` is called. |
| 683 | pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { | 986 | pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf(function)).@"fn".return_type.?) { |
| ... | @@ -685,7 +988,7 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( | ... | @@ -685,7 +988,7 @@ pub fn async(io: Io, function: anytype, args: anytype) Future(@typeInfo(@TypeOf( |
| 685 | const Args = @TypeOf(args); | 988 | const Args = @TypeOf(args); |
| 686 | const TypeErased = struct { | 989 | const TypeErased = struct { |
| 687 | fn start(context: *const anyopaque, result: *anyopaque) void { | 990 | fn start(context: *const anyopaque, result: *anyopaque) void { |
| 688 | const args_casted: *const Args = @alignCast(@ptrCast(context)); | 991 | const args_casted: *const Args = @ptrCast(@alignCast(context)); |
| 689 | const result_casted: *Result = @ptrCast(@alignCast(result)); | 992 | const result_casted: *Result = @ptrCast(@alignCast(result)); |
| 690 | result_casted.* = @call(.auto, function, args_casted.*); | 993 | result_casted.* = @call(.auto, function, args_casted.*); |
| 691 | } | 994 | } |
lib/std/Io/EventLoop.zig+7-7| ... | @@ -102,7 +102,7 @@ const Fiber = struct { | ... | @@ -102,7 +102,7 @@ const Fiber = struct { |
| 102 | return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); | 102 | return @ptrFromInt(alignment.forward(@intFromPtr(f) + @sizeOf(Fiber))); |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{AsyncCancel}!void { | 105 | fn enterCancelRegion(fiber: *Fiber, thread: *Thread) error{Canceled}!void { |
| 106 | if (@cmpxchgStrong( | 106 | if (@cmpxchgStrong( |
| 107 | ?*Thread, | 107 | ?*Thread, |
| 108 | &fiber.cancel_thread, | 108 | &fiber.cancel_thread, |
| ... | @@ -112,7 +112,7 @@ const Fiber = struct { | ... | @@ -112,7 +112,7 @@ const Fiber = struct { |
| 112 | .acquire, | 112 | .acquire, |
| 113 | )) |cancel_thread| { | 113 | )) |cancel_thread| { |
| 114 | assert(cancel_thread == Thread.canceling); | 114 | assert(cancel_thread == Thread.canceling); |
| 115 | return error.AsyncCancel; | 115 | return error.Canceled; |
| 116 | } | 116 | } |
| 117 | } | 117 | } |
| 118 | 118 | ||
| ... | @@ -746,7 +746,7 @@ pub fn createFile( | ... | @@ -746,7 +746,7 @@ pub fn createFile( |
| 746 | switch (errno(completion.result)) { | 746 | switch (errno(completion.result)) { |
| 747 | .SUCCESS => return .{ .handle = completion.result }, | 747 | .SUCCESS => return .{ .handle = completion.result }, |
| 748 | .INTR => unreachable, | 748 | .INTR => unreachable, |
| 749 | .CANCELED => return error.AsyncCancel, | 749 | .CANCELED => return error.Canceled, |
| 750 | 750 | ||
| 751 | .FAULT => unreachable, | 751 | .FAULT => unreachable, |
| 752 | .INVAL => return error.BadPathName, | 752 | .INVAL => return error.BadPathName, |
| ... | @@ -854,7 +854,7 @@ pub fn openFile( | ... | @@ -854,7 +854,7 @@ pub fn openFile( |
| 854 | switch (errno(completion.result)) { | 854 | switch (errno(completion.result)) { |
| 855 | .SUCCESS => return .{ .handle = completion.result }, | 855 | .SUCCESS => return .{ .handle = completion.result }, |
| 856 | .INTR => unreachable, | 856 | .INTR => unreachable, |
| 857 | .CANCELED => return error.AsyncCancel, | 857 | .CANCELED => return error.Canceled, |
| 858 | 858 | ||
| 859 | .FAULT => unreachable, | 859 | .FAULT => unreachable, |
| 860 | .INVAL => return error.BadPathName, | 860 | .INVAL => return error.BadPathName, |
| ... | @@ -950,7 +950,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std | ... | @@ -950,7 +950,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std |
| 950 | switch (errno(completion.result)) { | 950 | switch (errno(completion.result)) { |
| 951 | .SUCCESS => return @as(u32, @bitCast(completion.result)), | 951 | .SUCCESS => return @as(u32, @bitCast(completion.result)), |
| 952 | .INTR => unreachable, | 952 | .INTR => unreachable, |
| 953 | .CANCELED => return error.AsyncCancel, | 953 | .CANCELED => return error.Canceled, |
| 954 | 954 | ||
| 955 | .INVAL => unreachable, | 955 | .INVAL => unreachable, |
| 956 | .FAULT => unreachable, | 956 | .FAULT => unreachable, |
| ... | @@ -1002,7 +1002,7 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs | ... | @@ -1002,7 +1002,7 @@ pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offs |
| 1002 | switch (errno(completion.result)) { | 1002 | switch (errno(completion.result)) { |
| 1003 | .SUCCESS => return @as(u32, @bitCast(completion.result)), | 1003 | .SUCCESS => return @as(u32, @bitCast(completion.result)), |
| 1004 | .INTR => unreachable, | 1004 | .INTR => unreachable, |
| 1005 | .CANCELED => return error.AsyncCancel, | 1005 | .CANCELED => return error.Canceled, |
| 1006 | 1006 | ||
| 1007 | .INVAL => return error.InvalidArgument, | 1007 | .INVAL => return error.InvalidArgument, |
| 1008 | .FAULT => unreachable, | 1008 | .FAULT => unreachable, |
| ... | @@ -1080,7 +1080,7 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D | ... | @@ -1080,7 +1080,7 @@ pub fn sleep(userdata: ?*anyopaque, clockid: std.posix.clockid_t, deadline: Io.D |
| 1080 | switch (errno(completion.result)) { | 1080 | switch (errno(completion.result)) { |
| 1081 | .SUCCESS, .TIME => return, | 1081 | .SUCCESS, .TIME => return, |
| 1082 | .INTR => unreachable, | 1082 | .INTR => unreachable, |
| 1083 | .CANCELED => return error.AsyncCancel, | 1083 | .CANCELED => return error.Canceled, |
| 1084 | 1084 | ||
| 1085 | else => |err| return std.posix.unexpectedErrno(err), | 1085 | else => |err| return std.posix.unexpectedErrno(err), |
| 1086 | } | 1086 | } |
lib/std/Thread/Pool.zig+179-8| ... | @@ -332,9 +332,12 @@ pub fn io(pool: *Pool) Io { | ... | @@ -332,9 +332,12 @@ pub fn io(pool: *Pool) Io { |
| 332 | .vtable = &.{ | 332 | .vtable = &.{ |
| 333 | .@"async" = @"async", | 333 | .@"async" = @"async", |
| 334 | .@"await" = @"await", | 334 | .@"await" = @"await", |
| 335 | |||
| 336 | .cancel = cancel, | 335 | .cancel = cancel, |
| 337 | .cancelRequested = cancelRequested, | 336 | .cancelRequested = cancelRequested, |
| 337 | .mutexLock = mutexLock, | ||
| 338 | .mutexUnlock = mutexUnlock, | ||
| 339 | .conditionWait = conditionWait, | ||
| 340 | .conditionWake = conditionWake, | ||
| 338 | 341 | ||
| 339 | .createFile = createFile, | 342 | .createFile = createFile, |
| 340 | .openFile = openFile, | 343 | .openFile = openFile, |
| ... | @@ -517,11 +520,179 @@ fn cancelRequested(userdata: ?*anyopaque) bool { | ... | @@ -517,11 +520,179 @@ fn cancelRequested(userdata: ?*anyopaque) bool { |
| 517 | return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; | 520 | return @atomicLoad(std.Thread.Id, &closure.cancel_tid, .acquire) == AsyncClosure.canceling_tid; |
| 518 | } | 521 | } |
| 519 | 522 | ||
| 520 | fn checkCancel(pool: *Pool) error{AsyncCancel}!void { | 523 | fn checkCancel(pool: *Pool) error{Canceled}!void { |
| 521 | if (cancelRequested(pool)) return error.AsyncCancel; | 524 | if (cancelRequested(pool)) return error.Canceled; |
| 525 | } | ||
| 526 | |||
| 527 | fn mutexLock(userdata: ?*anyopaque, m: *Io.Mutex) void { | ||
| 528 | @branchHint(.cold); | ||
| 529 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 530 | _ = pool; | ||
| 531 | |||
| 532 | // Avoid doing an atomic swap below if we already know the state is contended. | ||
| 533 | // An atomic swap unconditionally stores which marks the cache-line as modified unnecessarily. | ||
| 534 | if (m.state.load(.monotonic) == Io.Mutex.contended) { | ||
| 535 | std.Thread.Futex.wait(&m.state, Io.Mutex.contended); | ||
| 536 | } | ||
| 537 | |||
| 538 | // Try to acquire the lock while also telling the existing lock holder that there are threads waiting. | ||
| 539 | // | ||
| 540 | // Once we sleep on the Futex, we must acquire the mutex using `contended` rather than `locked`. | ||
| 541 | // If not, threads sleeping on the Futex wouldn't see the state change in unlock and potentially deadlock. | ||
| 542 | // The downside is that the last mutex unlocker will see `contended` and do an unnecessary Futex wake | ||
| 543 | // but this is better than having to wake all waiting threads on mutex unlock. | ||
| 544 | // | ||
| 545 | // Acquire barrier ensures grabbing the lock happens before the critical section | ||
| 546 | // and that the previous lock holder's critical section happens before we grab the lock. | ||
| 547 | while (m.state.swap(Io.Mutex.contended, .acquire) != Io.Mutex.unlocked) { | ||
| 548 | std.Thread.Futex.wait(&m.state, Io.Mutex.contended); | ||
| 549 | } | ||
| 550 | } | ||
| 551 | |||
| 552 | fn mutexUnlock(userdata: ?*anyopaque, m: *Io.Mutex) void { | ||
| 553 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 554 | _ = pool; | ||
| 555 | // Needs to also wake up a waiting thread if any. | ||
| 556 | // | ||
| 557 | // A waiting thread will acquire with `contended` instead of `locked` | ||
| 558 | // which ensures that it wakes up another thread on the next unlock(). | ||
| 559 | // | ||
| 560 | // Release barrier ensures the critical section happens before we let go of the lock | ||
| 561 | // and that our critical section happens before the next lock holder grabs the lock. | ||
| 562 | const state = m.state.swap(Io.Mutex.unlocked, .release); | ||
| 563 | assert(state != Io.Mutex.unlocked); | ||
| 564 | |||
| 565 | if (state == Io.Mutex.contended) { | ||
| 566 | std.Thread.Futex.wake(&m.state, 1); | ||
| 567 | } | ||
| 568 | } | ||
| 569 | |||
| 570 | fn mutexLockInternal(pool: *std.Thread.Pool, m: *Io.Mutex) void { | ||
| 571 | if (!m.tryLock()) { | ||
| 572 | @branchHint(.unlikely); | ||
| 573 | mutexLock(pool, m); | ||
| 574 | } | ||
| 575 | } | ||
| 576 | |||
| 577 | fn conditionWait( | ||
| 578 | userdata: ?*anyopaque, | ||
| 579 | cond: *Io.Condition, | ||
| 580 | mutex: *Io.Mutex, | ||
| 581 | timeout: ?u64, | ||
| 582 | ) Io.Condition.WaitError!void { | ||
| 583 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 584 | comptime assert(@TypeOf(cond.state) == u64); | ||
| 585 | const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); | ||
| 586 | const cond_state = &ints[0]; | ||
| 587 | const cond_epoch = &ints[1]; | ||
| 588 | const one_waiter = 1; | ||
| 589 | const waiter_mask = 0xffff; | ||
| 590 | const one_signal = 1 << 16; | ||
| 591 | const signal_mask = 0xffff << 16; | ||
| 592 | // Observe the epoch, then check the state again to see if we should wake up. | ||
| 593 | // The epoch must be observed before we check the state or we could potentially miss a wake() and deadlock: | ||
| 594 | // | ||
| 595 | // - T1: s = LOAD(&state) | ||
| 596 | // - T2: UPDATE(&s, signal) | ||
| 597 | // - T2: UPDATE(&epoch, 1) + FUTEX_WAKE(&epoch) | ||
| 598 | // - T1: e = LOAD(&epoch) (was reordered after the state load) | ||
| 599 | // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed the state update + the epoch change) | ||
| 600 | // | ||
| 601 | // Acquire barrier to ensure the epoch load happens before the state load. | ||
| 602 | var epoch = cond_epoch.load(.acquire); | ||
| 603 | var state = cond_state.fetchAdd(one_waiter, .monotonic); | ||
| 604 | assert(state & waiter_mask != waiter_mask); | ||
| 605 | state += one_waiter; | ||
| 606 | |||
| 607 | mutexUnlock(pool, mutex); | ||
| 608 | defer mutexLockInternal(pool, mutex); | ||
| 609 | |||
| 610 | var futex_deadline = std.Thread.Futex.Deadline.init(timeout); | ||
| 611 | |||
| 612 | while (true) { | ||
| 613 | futex_deadline.wait(cond_epoch, epoch) catch |err| switch (err) { | ||
| 614 | // On timeout, we must decrement the waiter we added above. | ||
| 615 | error.Timeout => { | ||
| 616 | while (true) { | ||
| 617 | // If there's a signal when we're timing out, consume it and report being woken up instead. | ||
| 618 | // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. | ||
| 619 | while (state & signal_mask != 0) { | ||
| 620 | const new_state = state - one_waiter - one_signal; | ||
| 621 | state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; | ||
| 622 | } | ||
| 623 | |||
| 624 | // Remove the waiter we added and officially return timed out. | ||
| 625 | const new_state = state - one_waiter; | ||
| 626 | state = cond_state.cmpxchgWeak(state, new_state, .monotonic, .monotonic) orelse return err; | ||
| 627 | } | ||
| 628 | }, | ||
| 629 | }; | ||
| 630 | |||
| 631 | epoch = cond_epoch.load(.acquire); | ||
| 632 | state = cond_state.load(.monotonic); | ||
| 633 | |||
| 634 | // Try to wake up by consuming a signal and decremented the waiter we added previously. | ||
| 635 | // Acquire barrier ensures code before the wake() which added the signal happens before we decrement it and return. | ||
| 636 | while (state & signal_mask != 0) { | ||
| 637 | const new_state = state - one_waiter - one_signal; | ||
| 638 | state = cond_state.cmpxchgWeak(state, new_state, .acquire, .monotonic) orelse return; | ||
| 639 | } | ||
| 640 | } | ||
| 641 | } | ||
| 642 | |||
| 643 | fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, notify: Io.Condition.Notify) void { | ||
| 644 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | ||
| 645 | _ = pool; | ||
| 646 | comptime assert(@TypeOf(cond.state) == u64); | ||
| 647 | const ints: *[2]std.atomic.Value(u32) = @ptrCast(&cond.state); | ||
| 648 | const cond_state = &ints[0]; | ||
| 649 | const cond_epoch = &ints[1]; | ||
| 650 | const one_waiter = 1; | ||
| 651 | const waiter_mask = 0xffff; | ||
| 652 | const one_signal = 1 << 16; | ||
| 653 | const signal_mask = 0xffff << 16; | ||
| 654 | var state = cond_state.load(.monotonic); | ||
| 655 | while (true) { | ||
| 656 | const waiters = (state & waiter_mask) / one_waiter; | ||
| 657 | const signals = (state & signal_mask) / one_signal; | ||
| 658 | |||
| 659 | // Reserves which waiters to wake up by incrementing the signals count. | ||
| 660 | // Therefore, the signals count is always less than or equal to the waiters count. | ||
| 661 | // We don't need to Futex.wake if there's nothing to wake up or if other wake() threads have reserved to wake up the current waiters. | ||
| 662 | const wakeable = waiters - signals; | ||
| 663 | if (wakeable == 0) { | ||
| 664 | return; | ||
| 665 | } | ||
| 666 | |||
| 667 | const to_wake = switch (notify) { | ||
| 668 | .one => 1, | ||
| 669 | .all => wakeable, | ||
| 670 | }; | ||
| 671 | |||
| 672 | // Reserve the amount of waiters to wake by incrementing the signals count. | ||
| 673 | // Release barrier ensures code before the wake() happens before the signal it posted and consumed by the wait() threads. | ||
| 674 | const new_state = state + (one_signal * to_wake); | ||
| 675 | state = cond_state.cmpxchgWeak(state, new_state, .release, .monotonic) orelse { | ||
| 676 | // Wake up the waiting threads we reserved above by changing the epoch value. | ||
| 677 | // NOTE: a waiting thread could miss a wake up if *exactly* ((1<<32)-1) wake()s happen between it observing the epoch and sleeping on it. | ||
| 678 | // This is very unlikely due to how many precise amount of Futex.wake() calls that would be between the waiting thread's potential preemption. | ||
| 679 | // | ||
| 680 | // Release barrier ensures the signal being added to the state happens before the epoch is changed. | ||
| 681 | // If not, the waiting thread could potentially deadlock from missing both the state and epoch change: | ||
| 682 | // | ||
| 683 | // - T2: UPDATE(&epoch, 1) (reordered before the state change) | ||
| 684 | // - T1: e = LOAD(&epoch) | ||
| 685 | // - T1: s = LOAD(&state) | ||
| 686 | // - T2: UPDATE(&state, signal) + FUTEX_WAKE(&epoch) | ||
| 687 | // - T1: s & signals == 0 -> FUTEX_WAIT(&epoch, e) (missed both epoch change and state change) | ||
| 688 | _ = cond_epoch.fetchAdd(1, .release); | ||
| 689 | std.Thread.Futex.wake(cond_epoch, to_wake); | ||
| 690 | return; | ||
| 691 | }; | ||
| 692 | } | ||
| 522 | } | 693 | } |
| 523 | 694 | ||
| 524 | pub fn createFile( | 695 | fn createFile( |
| 525 | userdata: ?*anyopaque, | 696 | userdata: ?*anyopaque, |
| 526 | dir: std.fs.Dir, | 697 | dir: std.fs.Dir, |
| 527 | sub_path: []const u8, | 698 | sub_path: []const u8, |
| ... | @@ -532,7 +703,7 @@ pub fn createFile( | ... | @@ -532,7 +703,7 @@ pub fn createFile( |
| 532 | return dir.createFile(sub_path, flags); | 703 | return dir.createFile(sub_path, flags); |
| 533 | } | 704 | } |
| 534 | 705 | ||
| 535 | pub fn openFile( | 706 | fn openFile( |
| 536 | userdata: ?*anyopaque, | 707 | userdata: ?*anyopaque, |
| 537 | dir: std.fs.Dir, | 708 | dir: std.fs.Dir, |
| 538 | sub_path: []const u8, | 709 | sub_path: []const u8, |
| ... | @@ -543,13 +714,13 @@ pub fn openFile( | ... | @@ -543,13 +714,13 @@ pub fn openFile( |
| 543 | return dir.openFile(sub_path, flags); | 714 | return dir.openFile(sub_path, flags); |
| 544 | } | 715 | } |
| 545 | 716 | ||
| 546 | pub fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { | 717 | fn closeFile(userdata: ?*anyopaque, file: std.fs.File) void { |
| 547 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 718 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 548 | _ = pool; | 719 | _ = pool; |
| 549 | return file.close(); | 720 | return file.close(); |
| 550 | } | 721 | } |
| 551 | 722 | ||
| 552 | pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { | 723 | fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std.posix.off_t) Io.FilePReadError!usize { |
| 553 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 724 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 554 | try pool.checkCancel(); | 725 | try pool.checkCancel(); |
| 555 | return switch (offset) { | 726 | return switch (offset) { |
| ... | @@ -558,7 +729,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std | ... | @@ -558,7 +729,7 @@ pub fn pread(userdata: ?*anyopaque, file: std.fs.File, buffer: []u8, offset: std |
| 558 | }; | 729 | }; |
| 559 | } | 730 | } |
| 560 | 731 | ||
| 561 | pub fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { | 732 | fn pwrite(userdata: ?*anyopaque, file: std.fs.File, buffer: []const u8, offset: std.posix.off_t) Io.FilePWriteError!usize { |
| 562 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); | 733 | const pool: *std.Thread.Pool = @alignCast(@ptrCast(userdata)); |
| 563 | try pool.checkCancel(); | 734 | try pool.checkCancel(); |
| 564 | return switch (offset) { | 735 | return switch (offset) { |