Async-friendly read-write lock

I’m looking for a library that implements async/await-friendly read-write lock. I.e., the one which stores continuations instead of blocking threads. Any recommendations?

Do custom executors allow this?

2 Likes

Have you seen @gwendal.roue’s Semaphore?

2 Likes

Thanks, that will be useful as a building block or an inspiration.

Anything similar, but with a ready-to-use read-write lock?

Apologies for the late reply, I am really busy these days. I assume you usually have to write your own. There are a lot of factors that affect the design. For example, whether a FIFO order is important or whether it is write heavy or read heavy or how contested and performance critical it is going to be.

Here is a quick and dirty sketch of multiple readers / single writer I wrote to try out `swift-atomics` package.
import Atomics

/// A simple container class that uses *multiple readers / single writer* concurrency pattern to protect the value stored in it.
///
/// Obviously, the instances of this class are `Sendable`.
///
public final class ReadersWriter<Value>: @unchecked Sendable {
    
    // The storage for the protected value
    private var _value: Value

    // Atomic counter to implement Readers/Writer Lock
    private let readerCount = ManagedAtomic(0)
    
    // Flag for writing state
    private let WRITING = -1
        
    // Add a reader
    private func beginReading() async {
        var done = false
        var count = 0
        while true {
            count = readerCount.load(ordering: .relaxed)
            if count != WRITING {
                (done,count) = readerCount.weakCompareExchange(expected: count, desired: count+1, ordering: .acquiringAndReleasing)
                if done { break }
            }
            await Task.yield()
        }
    }
    
    // Remove a reader
    private func doneReading() { readerCount.wrappingDecrement(ordering: .acquiringAndReleasing) }

    // Enter writing state:
    private func signalWriting() async {
        while true {
            let (done,_) = readerCount.weakCompareExchange(expected: 0, desired: WRITING, ordering: .acquiringAndReleasing)
            if done { break }
            await Task.yield()
        }
    }
    
    // Leave writing state
    private func doneWriting() { readerCount.store(0, ordering: .releasing) }

    // The protected value as an async read only property
    public var value: Value {
        get async {
            await beginReading()
            defer { doneReading() }
            return _value
        }
    }
        
    /// The async method to set the value.
    ///
    /// Once we get async `set` feature for properties, it will become the setter of the `value` property.
    ///
    /// - Parameters:
    ///   - value: The new value to set
    /// - Returns: The old value that was overwritten
    ///
    @discardableResult
    public func set(to value: Value) async -> Value {
        await signalWriting()
        let oldValue = _value
        _value = value
        doneWriting()
        return oldValue
    }
    
    /// An async method to get the current value and transform it to the new value.
    ///
    /// Rethrows the error thrown by `transform` closure.
    ///
    /// - Parameters:
    ///   - transform: A potentilly async and throwing closure that receives the current value and return the new value.
    ///
    public func update(_ transform: (Value) async throws -> Value ) async rethrows {
        await signalWriting()
        defer { doneWriting() }
        _value = try await transform(_value)
    }
    
    /// An async method to mutate the value in-place
    ///
    /// Rethrows the error thrown by `transform` closure.
    ///
    /// - Parameters:
    ///   - transform: A potentilly throwing closure that receives the mutable value to update.
    public func mutate(_ transform: (inout Value) throws -> Void ) async rethrows {
        await signalWriting()
        defer { doneWriting() }
        try withUnsafeMutablePointer(to: &_value) { valuePtr in // FIXME!
            try transform(&valuePtr.pointee)
        }
    }
    
    /// Creates a `ReadersWriter` instance.
    ///
    /// - Parameters:
    ///   - value: The initial value.
    ///
    public init(_ value: Value) { _value = value }
}

I wrote it in an hour and tested it for a couple of minutes, so use it at your own risk.