mirror of
https://github.com/ziglang/zig.git
synced 2025-12-08 07:13:08 +00:00
This is a breaking change. Before, usage looked like this: ```zig const held = mutex.acquire(); defer held.release(); ``` Now it looks like this: ```zig mutex.lock(); defer mutex.unlock(); ``` The `Held` type was an idea to make mutexes slightly safer by making it more difficult to forget to release an aquired lock. However, this ultimately caused more problems than it solved, when any data structures needed to store a held mutex. Simplify everything by reducing the API down to the primitives: lock() and unlock(). Closes #8051 Closes #8246 Closes #10105
163 lines
5.0 KiB
Zig
163 lines
5.0 KiB
Zig
const std = @import("../std.zig");
|
|
const builtin = @import("builtin");
|
|
const assert = std.debug.assert;
|
|
const testing = std.testing;
|
|
const mem = std.mem;
|
|
const Loop = std.event.Loop;
|
|
|
|
/// Thread-safe async/await lock.
|
|
/// Functions which are waiting for the lock are suspended, and
|
|
/// are resumed when the lock is released, in order.
|
|
/// Allows only one actor to hold the lock.
|
|
/// TODO: make this API also work in blocking I/O mode.
|
|
pub const Lock = struct {
|
|
mutex: std.Thread.Mutex = std.Thread.Mutex{},
|
|
head: usize = UNLOCKED,
|
|
|
|
const UNLOCKED = 0;
|
|
const LOCKED = 1;
|
|
|
|
const global_event_loop = Loop.instance orelse
|
|
@compileError("std.event.Lock currently only works with event-based I/O");
|
|
|
|
const Waiter = struct {
|
|
// forced Waiter alignment to ensure it doesn't clash with LOCKED
|
|
next: ?*Waiter align(2),
|
|
tail: *Waiter,
|
|
node: Loop.NextTickNode,
|
|
};
|
|
|
|
pub fn initLocked() Lock {
|
|
return Lock{ .head = LOCKED };
|
|
}
|
|
|
|
pub fn acquire(self: *Lock) Held {
|
|
self.mutex.lock();
|
|
|
|
// self.head transitions from multiple stages depending on the value:
|
|
// UNLOCKED -> LOCKED:
|
|
// acquire Lock ownership when theres no waiters
|
|
// LOCKED -> <Waiter head ptr>:
|
|
// Lock is already owned, enqueue first Waiter
|
|
// <head ptr> -> <head ptr>:
|
|
// Lock is owned with pending waiters. Push our waiter to the queue.
|
|
|
|
if (self.head == UNLOCKED) {
|
|
self.head = LOCKED;
|
|
self.mutex.unlock();
|
|
return Held{ .lock = self };
|
|
}
|
|
|
|
var waiter: Waiter = undefined;
|
|
waiter.next = null;
|
|
waiter.tail = &waiter;
|
|
|
|
const head = switch (self.head) {
|
|
UNLOCKED => unreachable,
|
|
LOCKED => null,
|
|
else => @intToPtr(*Waiter, self.head),
|
|
};
|
|
|
|
if (head) |h| {
|
|
h.tail.next = &waiter;
|
|
h.tail = &waiter;
|
|
} else {
|
|
self.head = @ptrToInt(&waiter);
|
|
}
|
|
|
|
suspend {
|
|
waiter.node = Loop.NextTickNode{
|
|
.prev = undefined,
|
|
.next = undefined,
|
|
.data = @frame(),
|
|
};
|
|
self.mutex.unlock();
|
|
}
|
|
|
|
return Held{ .lock = self };
|
|
}
|
|
|
|
pub const Held = struct {
|
|
lock: *Lock,
|
|
|
|
pub fn release(self: Held) void {
|
|
const waiter = blk: {
|
|
self.lock.mutex.lock();
|
|
defer self.lock.mutex.unlock();
|
|
|
|
// self.head goes through the reverse transition from acquire():
|
|
// <head ptr> -> <new head ptr>:
|
|
// pop a waiter from the queue to give Lock ownership when theres still others pending
|
|
// <head ptr> -> LOCKED:
|
|
// pop the laster waiter from the queue, while also giving it lock ownership when awaken
|
|
// LOCKED -> UNLOCKED:
|
|
// last lock owner releases lock while no one else is waiting for it
|
|
|
|
switch (self.lock.head) {
|
|
UNLOCKED => {
|
|
unreachable; // Lock unlocked while unlocking
|
|
},
|
|
LOCKED => {
|
|
self.lock.head = UNLOCKED;
|
|
break :blk null;
|
|
},
|
|
else => {
|
|
const waiter = @intToPtr(*Waiter, self.lock.head);
|
|
self.lock.head = if (waiter.next == null) LOCKED else @ptrToInt(waiter.next);
|
|
if (waiter.next) |next|
|
|
next.tail = waiter.tail;
|
|
break :blk waiter;
|
|
},
|
|
}
|
|
};
|
|
|
|
if (waiter) |w| {
|
|
global_event_loop.onNextTick(&w.node);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
test "std.event.Lock" {
|
|
if (!std.io.is_async) return error.SkipZigTest;
|
|
|
|
// TODO https://github.com/ziglang/zig/issues/1908
|
|
if (builtin.single_threaded) return error.SkipZigTest;
|
|
|
|
// TODO https://github.com/ziglang/zig/issues/3251
|
|
if (builtin.os.tag == .freebsd) return error.SkipZigTest;
|
|
|
|
var lock = Lock{};
|
|
testLock(&lock);
|
|
|
|
const expected_result = [1]i32{3 * @intCast(i32, shared_test_data.len)} ** shared_test_data.len;
|
|
try testing.expectEqualSlices(i32, &expected_result, &shared_test_data);
|
|
}
|
|
fn testLock(lock: *Lock) void {
|
|
var handle1 = async lockRunner(lock);
|
|
var handle2 = async lockRunner(lock);
|
|
var handle3 = async lockRunner(lock);
|
|
|
|
await handle1;
|
|
await handle2;
|
|
await handle3;
|
|
}
|
|
|
|
var shared_test_data = [1]i32{0} ** 10;
|
|
var shared_test_index: usize = 0;
|
|
|
|
fn lockRunner(lock: *Lock) void {
|
|
Lock.global_event_loop.yield();
|
|
|
|
var i: usize = 0;
|
|
while (i < shared_test_data.len) : (i += 1) {
|
|
const handle = lock.acquire();
|
|
defer handle.release();
|
|
|
|
shared_test_index = 0;
|
|
while (shared_test_index < shared_test_data.len) : (shared_test_index += 1) {
|
|
shared_test_data[shared_test_index] = shared_test_data[shared_test_index] + 1;
|
|
}
|
|
}
|
|
}
|