This thread was really informative, thank you.
I'm currently attempting to use Mutex
in some caching logic and wondered if a property wrapper would also be viable considering the _read and _modify accessors.
Since it was mentioned that anyone could replicate Mutex, I gave it a shot. These experimental features were needed: BuiltinModule
, RawLayout
, and BuiltinAddressOfRawLayout
. I rebuilt Mutex nearly line for line (though I renamed _Cell
to UnsafeStablePointer
and _MutexHandle
to UnfairLock
for my own brain):
@propertyWrapper public struct Mutex<Value: ~Copyable> : ~Copyable {
let value: UnsafeStablePointer<Value>
let handle = UnfairLock()
public init(wrappedValue: consuming sending Value) {
value = .init(wrappedValue)
}
public var wrappedValue: Value {
_read {
handle.lock()
defer {
handle.unlock()
}
yield value._address.pointee
}
_modify {
handle.lock()
defer {
handle.unlock()
}
yield &value._address.pointee
}
}
}
extension Mutex: @unchecked Sendable where Value: ~Copyable {}
Implementations Continued...
@frozen
@_rawLayout(like: Value, movesAsLike)
public struct UnsafeStablePointer<Value: ~Copyable> : ~Copyable {
@_alwaysEmitIntoClient
@_transparent
public var _address: UnsafeMutablePointer<Value> {
.init(pointer)
}
@_alwaysEmitIntoClient
@_transparent
var pointer: Builtin.RawPointer {
Builtin.addressOfRawLayout(self)
}
@_alwaysEmitIntoClient
@_transparent
public init(_ initialValue: consuming Value) {
_address.initialize(to: initialValue)
}
@_alwaysEmitIntoClient
@inlinable
deinit {
_address.deinitialize(count: 1)
}
}
public struct UnfairLock: ~Copyable {
let value: UnsafeStablePointer<os_unfair_lock>
public init() {
value = .init(os_unfair_lock())
}
public borrowing func lock() {
os_unfair_lock_lock(value._address)
}
public borrowing func tryLock() -> Bool {
os_unfair_lock_trylock(value._address)
}
public borrowing func unlock() {
os_unfair_lock_unlock(value._address)
}
}
Are there any upsides to this take rather than the closure base API that seems to be preferred? I ran into some issues while trying to implement a withLock(_ body:)
function:
public var projectedValue: Mutex<Value> { /// 🛑 'self' is borrowed and cannot be consumed
self
}
public borrowing func withLock<R: ~Copyable, E: Error>(_ body: (inout sending Value) throws(E) -> sending R) throws(E) -> sending R {
handle.lock()
defer { handle.unlock() }
return try body(&value._address.pointee)
}
I know that here be dragons, and that I'm kinda standing right in the mouth of the cave.
My implementation is nearly the same as Synchronization's Mutex
type so I'm ready to fallback to it. However... in the same vein of the original question, I'm curious as to why the API is closure based?
When I was doing some research I found out some caveats regarding atomic read and write. For example, subscripts and mutating properties that would use separate lock/unlock calls for get
and set
. Dictionary's key subscript being the go-to example.
To my naive eyes, this is something _modify
and property wrappers were designed to fix. Would I be shooting myself in the foot with this property wrapper implementation?