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.

1 Like

Thanks for the sample @hooman. I had this working using swift-atomics, but unfortunately that library has issues when compiled as a transitive dependency, so I'm unable to use it for the time being.

Has anyone come up with a way to do this that doesn't involve swift-atomics yet? @gwendal.roue's AsyncSemaphore is great, but I don't think it works for this scenario, since we need to allow concurrent reads.

I wrote this thing as an exercise last week.. it is mostly untested and unreviewed (and could I suppose have important flaws). Look at the queuedRead and a queuedWrite methods for async/await friendliness.

Inspired from Simon Whitty's backported Mutex.

import Dispatch

/// DispatchQueue-based multiple-reader single-writer mutual exclusion protecting a mutable value.
/// Value can be accessed by locking synchronously or enqueuing tasks to run asynchrounsly on the queue.
/// Reads can run concurrently and writes are run serially with other write or read operations.
public struct DispatchQueueRWMutex<Value: ~Copyable>: ~Copyable, Sendable {
	private let storage: UncheckedSendableStorage<Value>
	let queue: DispatchQueue

	public init(_ initialValue: consuming sending Value, label: String, qos: DispatchQoS = .unspecified, autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil) {
		self.storage = UncheckedSendableStorage(initialValue)
		// note: using .concurrent attribute because we want concurrent reads
		// and using .barrier flag for writes to ensure exclusivity
		self.queue = DispatchQueue(label: label, qos: qos, attributes: [.concurrent], autoreleaseFrequency: autoreleaseFrequency, target: target)
	}

	public borrowing func withReadLock<Result, E: Error>(
		_ body: (borrowing Value) throws(E) -> sending Result
	) throws(E) -> sending Result {
		var result = UncheckedSendableResult<Result, E>()
		queue.sync { [storage] in
			result.set { () throws(E) -> Result in
				try body(storage.value)
			}
		}
		return try result.get()
	}

	public borrowing func withWriteLock<Result, E: Error>(
		_ body: (inout sending Value) throws(E) -> sending Result
	) throws(E) -> sending Result {
		var result = UncheckedSendableResult<Result, E>()
		queue.sync(flags: [.barrier]) { [storage] in
			result.set { () throws(E) -> Result in
				try body(&storage.value)
			}
		}
		return try result.get()
	}

	public borrowing func enqueueRead(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], _ body: sending @escaping (borrowing Value) -> ()) {
		queue.async(group: group, qos: qos, flags: flags) { [storage] in
			body(storage.value)
		}
	}
	public borrowing func enqueueWrite(group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], _ body: sending @escaping (inout sending Value) -> ()) {
		queue.async(group: group, qos: qos, flags: [flags, .barrier]) { [storage] in
			body(&storage.value)
		}
	}

	public borrowing func enqueueReadAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], _ body: sending @escaping (borrowing Value) -> ()) {
		queue.asyncAfter(deadline: deadline, qos: qos, flags: flags) { [storage] in
			body(storage.value)
		}
	}
	public borrowing func enqueueWriteAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], _ body: sending @escaping (inout sending Value) -> ()) {
		queue.asyncAfter(deadline: deadline, qos: qos, flags: [flags, .barrier]) { [storage] in
			body(&storage.value)
		}
	}

	@available(macOS 10.15, *)
	public borrowing func queuedRead<Result, E: Error>(
		group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [],
		_ body: sending @escaping (borrowing Value) throws(E) -> sending Result
	) async throws(E) -> sending Result {
		var result = UncheckedSendableResult<Result, E>()
		await withCheckedContinuation { [storage] continuation in
			queue.async(group: group, qos: qos, flags: flags) {
				result.set { () throws(E) -> Result in
					try body(storage.value)
				}
				continuation.resume(returning: ())
			}
		}
		return try result.get()
	}
	@available(macOS 10.15, *)
	public borrowing func queuedWrite<Result, E: Error>(
		group: DispatchGroup? = nil, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [],
		_ body: sending @escaping (inout sending Value) throws(E) -> sending Result
	) async throws(E) -> sending Result {
		var result = UncheckedSendableResult<Result, E>()
		await withCheckedContinuation { [storage] continuation in
			queue.async(group: group, qos: qos, flags: [flags, .barrier]) {
				result.set { () throws(E) -> Result in
					try body(&storage.value)
				}
				continuation.resume(returning: ())
			}
		}
		return try result.get()
	}

}

private final class UncheckedSendableStorage<Value: ~Copyable>: @unchecked Sendable {
	nonisolated(unsafe) var value: Value

	init(_ value: consuming sending Value) {
		self.value = value
	}
}

private struct UncheckedSendableResult<Success, Failure: Error>: @unchecked Sendable {
	private var result: Result<Success, Failure>! = nil

	mutating func set(_ body: () throws(Failure) -> Success) {
		assert(result == nil, "Setting result twice.")
		result = Result(catching: body)
	}
	func get() throws(Failure) -> Success {
		assert(result != nil, "Getting before result is set.")
		return try result.get()
	}
}

Apple does not recommend using DispatchQueue as a lock. It's orders of magnitude more expensive to use this way than other available lock mechanisms and it's difficult to amortize its overhead with fine-grained calls. If you're using Swift with a deployment target older than Mutex, use OSAllocatedUnfairLock instead.

(I realize this doesn't answer the original question about async-friendly locks.)

There is indeed a blocking withLock method in my type to make it easier to migrate existing DispatchQueue-based code that could occasionally be calling sync. Of course if you use only withLock you'd be better using a real mutex.

All the other methods run the closure asynchronously on the queue. queuedRead and queuedWrite are the async/await friendly ones based on a continuation. I suppose used purely as a lock they will be slow since they are unconditionally asynchronous. Ideally you'd suspend the current task only when the lock can't be acquired immediately (so after failing a tryLock). But for something based on DispatchQueue, it's the best I could come with.

Thanks @michelf. Unfortunately, this solution doesn't allow for awaiting on async functions within the lock. The only way I've managed to do that so far was with swift-atomics, which won't work due to the transitive dependency issue.

You could always use a regular integer protected by a mutex as an implementation detail to fake atomic operations on readerCount. If you're going to await inside the closure (while locked!) the extra overhead of using a mutex to protect the control value seems rather laughable.

Normally I'd say it's a bad idea to await while in a locked state, but I have no idea of your use case so I'll refrain from jumping to conclusions.

Yes, I don't think it would be safe to keep the locked state while running the await.

I ended up coming up with a solution that is passing all of my existing unit tests. I'm not sure if this is 100% perfect, so if anyone has any suggestions or would like to check my work, please let me know what you think.

This works by using an actor to keep track of Tasks for reads and writes. The tasks are awaited, but the actor should allow other calls to read() and write() to be dispatched while waiting for the task to complete. The actor isn't locked up while the task bodies are being executed, it just prevents read tasks from beginning during a write tasks and write tasks from beginning during a read or write task.

actor AsyncReadWriteLock {
  private final class ReadTask: Sendable {
    let task: Task<Void, any Swift.Error>

    init(_ body: @Sendable @escaping () async throws -> Void) {
      task = Task {
        try await body()
      }
    }
  }

  private var currentReadTasks: [ObjectIdentifier: ReadTask] = [:]
  private var currentWriteTask: Task<Void, any Swift.Error>?

  /// Waits for all current reads/writes to be completed, then calls the provided closure while preventing
  /// any other reads/writes from beginning.
  ///
  /// This function should be `rethrows` but the compiler doesn't understand that when passing the `block` into a Task.
  ///  If the `body` provided does not throw, this function will not throw.
  func write(_ body: @Sendable @escaping () async throws -> Void) async throws {
    while currentWriteTask != nil || !currentReadTasks.isEmpty {
      await Task.yield()
      continue
    }

    defer { currentWriteTask = nil }
    let writeTask = Task {
      try await body()
    }
    currentWriteTask = writeTask

    try await writeTask.value
  }

  /// Waits for all current writes to be completed, then calls the provided closure while preventing
  /// any other writes from beginning. Other reads may be executed concurrently.
  ///
  /// This function should be `rethrows` but the compiler doesn't understand that when passing the `block` into a Task.
  ///  If the `body` provided does not throw, this function will not throw.
  func read(_ body: @Sendable @escaping () async throws -> Void) async throws {
    while currentWriteTask != nil {
      await Task.yield()
      continue
    }

    let readTask = ReadTask(body)
    let taskID = ObjectIdentifier(readTask)
    defer { currentReadTasks[taskID] = nil }
    currentReadTasks[taskID] = readTask

    try await readTask.task.value
  }

}

Actors are inherently serial. Is this not equivalent to just tagging your “read” tasks with a higher QoS than your “write” tasks?

At first glance the exclusion logic works, but calling await Task.yield() in a loop sounds wasteful to the extreme. It's like a spin lock in that it'll consume CPU until the condition is met. It'll of course let other same- or higher-priority tasks (if any) run between the yields, but it'll steal a thread in the thread pool from lower priority tasks. Or at least that's what I think will happen.

And if the thread pool is all taken by those tasks spinning waiting to acquire the lock while the lock is held by a lower-priority task, then you'll get a priority inversion and a deadlock (or to be more precise a livelock since every thread will be spinning).

Actors are inherently serial. Is this not equivalent to just tagging your “read” tasks with a higher QoS than your “write” tasks?

My understanding was that the actor is not blocking during the call to try await body(). That's what causes the actor re-entrancy problem, but I'm taking advantage of that behavior here. It's only blocked during the time where we are checking for other concurrently running tasks, but not during the actual execution of those tasks.

I did just try running my unit tests for concurrent reads with breakpoints set, and it does look like I have multiple threads doing separate reads at the same time.

If I am misunderstanding how this works, someone please correct me!

At first glance the exclusion logic works, but calling await Task.yield() in a loop sounds wasteful to the extreme. It's like a spin lock in that it'll consume CPU until the condition is met. It'll of course let other same- or higher-priority tasks (if any) run between the yields, but it'll steal a thread in the thread pool from lower priority tasks. Or at least that's what I think will happen.

And if the thread pool is all taken by those tasks spinning waiting to acquire the lock while the lock is held by a lower-priority task, then you'll get a priority inversion and a deadlock (or to be more precise a livelock since every thread will be spinning).

Hmm... I see what you mean. I took the idea of using await Task.yield() from the example implementation using swift-atomics from @hooman in this comment.

It feels like there is really just no way to solve this problem safely then? Even the above referenced implementation that does use swift-atomics would run into this issue. Is that right?

Seems to me the atomic version above would also suffers from the same issues.

What you need is the ability to queue tasks without launching them immediately. I'd try creating a queue type managing the RW lock and only creating the Task once the lock is acquired:

await withCheckedContinuation { continuation in
   // queue a synchronous task until the lock can be acquired
   customQueue.queuedAquireReadLock {
      // running synchronously in context provided by the queue
      Task {
         // running async in context provided by Task
         await body() // async closure is called here
         // signal the caller we're done running the closure so it can resume
         continuation.resume()
         // now signal the queue we're done and it can start new pending tasks
         customQueue.unlockAndStartNextPendingTasks()
      }
   }
}

Now, that's probably going to mess with task-local values. I'm also not sure how to propagate priority correctly. But unless the queue is badly implemented your tasks aren't going to hog the CPU while waiting.

struct RWLock: ~Copyable {
  private struct Storage {
    enum ActiveState {
      case idle
      case reading(Int)
      case writing
    }

    var queue: [(handle: UnsafeContinuation<Void, Never>, isWriter: Bool)]
    var active: ActiveState
  }
  private let mutex: Mutex<Storage>

  public func withReadLock<R, E>(_ body: () async throws(E) -> R) throws(E) -> R {
    await withUnsafeContinuation { cont in
      let ready = mutex.withLock { storage in
        switch storage.active{
        case .idle:
          storage.active = .reading(1)
          return true

        // If we're currently reading, there should only be a queue if there's
        // at least one waiting writer. Don't jump the queue.
        case .reading(let n) where storage.queue.isEmpty:
          storage.active = .reading(n + 1)
          return true

        default:
          queue.append((handle: cont, isWriter: false))
          return false
        }
      }
      if ready { cont.resume(()) }
    }
    defer {
      mutex.withLock { storage in storage.wakeFromReading() }
    }
    return try await body()
  }

  public func withWriteLock<R, E>(_ body: () async throws(E) -> R) throws(E) -> R {
    await withUnsafeContinuation { cont in
      let ready = mutex.withLock { storage in
        switch storage.active{
        case .idle:
          storage.active = .writing
          return true

        default:
          queue.append((handle: cont, isWriter: true))
          return false
        }
      }
      if ready { cont.resume(()) }
    }
    defer {
      mutex.withLock { storage in storage.wakeFromWriting() }
    }
    return try await body()
  }
}

extension RWLock.Storage {
  mutating func wakeFromReading() {
    guard case .reading(let n) = active else {
      preconditionFailure("waking from reading, but state is not reading")
    }
    if n > 1 {
      active = .reading(n - 1)
    } else {
      wakeNext()
    }
  }

  mutating func wakeFromWriting() {
    guard case .writing = active else {
      preconditionFailure("waking from writing, but state is not writing")
    }
    wakeNext()
  }

  private mutating func wakeNext() {
    guard !queue.empty else {
      active = .idle
      return
    }
    queue[0].handle.resume(())
    if queue[0].isWriter {
      active = .writing
      queue.remove(at: 0)
    } else {
      var lastReader = 0
      for i in 1..<queue.count {
        guard !queue[i].isWriter else { break }
        queue[i].handle.resume(())
        lastReader = i
      }
      queue.removeSubrange(0...lastReader)
      active = .reading(lastReader + 1)
    }
  }
}

FIFO, doesn't avoid priority inversions, probably ought to use a deque, but it should work.

9 Likes

Sorry, but has anyone been able to compile the code above?

I am trying to understand how it works, but I couldn't even compile it (using Xcode 16.3.)

Here's a version of @John_McCall's RWLock concept above where I made some fixes so it compiles. Not tested beyond that though.

struct RWLock: ~Copyable {
	fileprivate struct Storage {
		enum ActiveState {
			case idle
			case reading(Int)
			case writing
		}

		var queue: [(handle: UnsafeContinuation<Void, Never>, isWriter: Bool)]
		var active: ActiveState
	}
	private let mutex: Mutex<Storage>

	public func withReadLock<R, E>(_ body: () async throws(E) -> R) async throws(E) -> R {
		await withUnsafeContinuation { cont -> Void in
			let ready = mutex.withLock { storage in
				switch storage.active{
				case .idle:
					storage.active = .reading(1)
					return true

					// If we're currently reading, there should only be a queue if there's
					// at least one waiting writer. Don't jump the queue.
				case .reading(let n) where storage.queue.isEmpty:
					storage.active = .reading(n + 1)
					return true

				default:
					storage.queue.append((handle: cont, isWriter: false))
					return false
				}
			}
			if ready { cont.resume(returning: ()) }
		}
		defer {
			mutex.withLock { storage in storage.wakeFromReading() }
		}
		return try await body()
	}

	public func withWriteLock<R, E>(_ body: () async throws(E) -> R) async throws(E) -> R {
		await withUnsafeContinuation { cont -> Void in
			let ready = mutex.withLock { storage in
				switch storage.active{
				case .idle:
					storage.active = .writing
					return true

				default:
					storage.queue.append((handle: cont, isWriter: true))
					return false
				}
			}
			if ready { cont.resume(returning: ()) }
		}
		defer {
			mutex.withLock { storage in storage.wakeFromWriting() }
		}
		return try await body()
	}
}

extension RWLock.Storage {
	mutating func wakeFromReading() {
		guard case .reading(let n) = active else {
			preconditionFailure("waking from reading, but state is not reading")
		}
		if n > 1 {
			active = .reading(n - 1)
		} else {
			wakeNext()
		}
	}

	mutating func wakeFromWriting() {
		guard case .writing = active else {
			preconditionFailure("waking from writing, but state is not writing")
		}
		wakeNext()
	}

	private mutating func wakeNext() {
		guard !queue.isEmpty else {
			active = .idle
			return
		}
		queue[0].handle.resume(returning: ())
		if queue[0].isWriter {
			active = .writing
			queue.remove(at: 0)
		} else {
			var lastReader = 0
			for i in 1..<queue.count {
				guard !queue[i].isWriter else { break }
				queue[i].handle.resume(returning: ())
				lastReader = i
			}
			queue.removeSubrange(0...lastReader)
			active = .reading(lastReader + 1)
		}
	}
}

How it works is that it creates a continuation just for the purpose waiting until the right moment. If it's not ready to continue immediately, it'll put the continuation on a queue for resuming at a later time. Every time a closure under the lock ends running, cleanup code will attempt to resume one or more pending continuations. Clever idea!

4 Likes

@michelf, thank you!

I got it compiled with the following tweaks:

struct RWLock: ~Copyable {
    fileprivate struct Storage {
        enum ActiveState {
            case idle
            case reading(Int)
            case writing
        }

        var queue: [(handle: UnsafeContinuation<Void, Never>, isWriter: Bool)] = []
        var active: ActiveState = .idle // Tweak 1
    }
    private let mutex: Mutex <Storage> = .init (.init ()) // Tweak 2

But, now I am getting the error: Command SwiftCompile failed with a nonzero exit code

Details

SwiftCompile normal x86_64 Compiling\ Driver.swift /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock/ReadWriteLock/Driver.swift (in target 'ReadWriteLock' from project 'ReadWriteLock')

Global is external, but doesn't have external or weak linkage!
ptr @"$s13ReadWriteLock6RWLockV7Storage33_8F860394EFD8E9DB55CC0D05B58E1903LLVMn"
Please submit a bug report (Swift.org - Contributing) and include the crash backtrace.
Stack dump:
0. Program arguments: /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock/ReadWriteLock/RWLock.swift -primary-file /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock/ReadWriteLock/Driver.swift -emit-dependencies-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.d -emit-const-values-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.swiftconstvalues -emit-reference-dependencies-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.swiftdeps -serialize-diagnostics-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.dia -target x86_64-apple-macos15.4 -enable-objc-interop -stack-check -sdk /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk -I /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug -F /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug -no-color-diagnostics -enable-testing -g -debug-info-format=dwarf -dwarf-version=5 -module-cache-path /Users/ibex/Library/Developer/Xcode/DerivedData/ModuleCache.noindex -swift-version 5 -enforce-exclusivity=checked -Onone -D DEBUG -serialize-debugging-options -const-gather-protocols-file /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/ReadWriteLock_const_extract_protocols.json -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -empty-abi-descriptor -validate-clang-modules-once -clang-build-session-file /Users/ibex/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/Session.modulevalidation -Xcc -working-directory -Xcc /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock -resource-dir /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift -enable-anonymous-context-mangled-names -file-compilation-dir /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock -Xcc -ivfsstatcache -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/SDKStatCaches.noindex/macosx15.4-24E241-01584612d9b39f8370399a8abc751519.sdkstatcache -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/swift-overrides.hmap -Xcc -iquote -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-generated-files.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-own-target-headers.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-all-target-headers.hmap -Xcc -iquote -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-project-headers.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug/include -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources-normal/x86_64 -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources/x86_64 -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources -Xcc -DDEBUG=1 -module-name ReadWriteLock -frontend-parseable-output -disable-clang-spi -target-sdk-version 15.4 -target-sdk-name macosx15.4 -external-plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins#/Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -external-plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/local/lib/swift/host/plugins#/Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -in-process-plugin-server-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/libSwiftInProcPluginServer.dylib -plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/plugins -plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/local/lib/swift/host/plugins -o /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.o -index-unit-output-path /ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.o -index-store-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Index.noindex/DataStore -index-system-modules

  1. Apple Swift version 6.1 (swiftlang-6.1.0.110.21 clang-1700.0.13.3)
  2. Compiling with effective version 5.10
  3. Running pass "verify" on module "/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.o"
    Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var LLVM_SYMBOLIZER_PATH to point to it):
    0 swift-frontend 0x0000000116dbe708 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 40
    1 swift-frontend 0x0000000116dbc039 llvm::sys::RunSignalHandlers() + 233
    2 swift-frontend 0x0000000116dbecee SignalHandler(int) + 302
    3 libsystem_platform.dylib 0x00007ff81594c25d _sigtramp + 29
    4 swift-frontend 0x000000011838cfba .str.15 + 553
    5 libsystem_c.dylib 0x00007ff81583273e abort + 126
    6 swift-frontend 0x0000000110104762 swift::DiagnosticHelper::Implementation::diagnoseFatalError(char const*, bool) + 786
    7 swift-frontend 0x0000000116d218fa llvm::report_fatal_error(llvm::Twine const&, bool) + 394
    8 swift-frontend 0x0000000116d21769 llvm::report_fatal_error(char const*, bool) + 41
    9 swift-frontend 0x00000001103cabde llvm::detail::PassModel<llvm::Module, llvm::VerifierPass, llvm::AnalysisManagerllvm::Module>::run(llvm::Module&, llvm::AnalysisManagerllvm::Module&) + 142
    10 swift-frontend 0x0000000116c1e7a5 llvm::PassManager<llvm::Module, llvm::AnalysisManagerllvm::Module>::run(llvm::Module&, llvm::AnalysisManagerllvm::Module&) + 1061
    11 swift-frontend 0x00000001103bbf89 swift::performLLVMOptimizations(swift::IRGenOptions const&, llvm::Module*, llvm::TargetMachine*, llvm::raw_pwrite_stream*) + 5689
    12 swift-frontend 0x00000001103bd6bd swift::performLLVM(swift::IRGenOptions const&, swift::DiagnosticEngine&, llvm::sys::SmartMutex, llvm::GlobalVariable, llvm::Module*, llvm::TargetMachine*, llvm::StringRef, llvm::vfs::OutputBackend&, swift::UnifiedStatsReporter*) + 2221
    13 swift-frontend 0x000000010fcefa26 generateCode(swift::CompilerInstance&, llvm::StringRef, llvm::Module*, llvm::GlobalVariable*) + 502
    14 swift-frontend 0x000000010fce9805 performCompileStepsPostSILGen(swift::CompilerInstance&, std::__1::unique_ptr<swift::SILModule, std::__1::default_deleteswift::SILModule>, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, swift::PrimarySpecificPaths const&, int&, swift::FrontendObserver*) + 2725
    15 swift-frontend 0x000000010fce835a swift::performCompileStepsPostSema(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 1242
    16 swift-frontend 0x000000010fcec82c performCompile(swift::CompilerInstance&, int&, swift::FrontendObserver*) + 3036
    17 swift-frontend 0x000000010fceaa16 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 4518
    18 swift-frontend 0x000000010fc59b09 swift::mainEntry(int, char const**) + 3241
    19 dyld 0x00007ff81556f530 start + 3056
    Failed frontend command:
    /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock/ReadWriteLock/RWLock.swift -primary-file /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock/ReadWriteLock/Driver.swift -emit-dependencies-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.d -emit-const-values-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.swiftconstvalues -emit-reference-dependencies-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.swiftdeps -serialize-diagnostics-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.dia -target x86_64-apple-macos15.4 -enable-objc-interop -stack-check -sdk /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk -I /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug -F /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug -no-color-diagnostics -enable-testing -g -debug-info-format=dwarf -dwarf-version=5 -module-cache-path /Users/ibex/Library/Developer/Xcode/DerivedData/ModuleCache.noindex -swift-version 5 -enforce-exclusivity=checked -Onone -D DEBUG -serialize-debugging-options -const-gather-protocols-file /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/ReadWriteLock_const_extract_protocols.json -enable-experimental-feature DebugDescriptionMacro -enable-bare-slash-regex -empty-abi-descriptor -validate-clang-modules-once -clang-build-session-file /Users/ibex/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/Session.modulevalidation -Xcc -working-directory -Xcc /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock -resource-dir /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift -enable-anonymous-context-mangled-names -file-compilation-dir /Users/ibex/A/Xcode/Work/Swift/ZZZ/SwiftJam/2025/ReadWriteLock -Xcc -ivfsstatcache -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/SDKStatCaches.noindex/macosx15.4-24E241-01584612d9b39f8370399a8abc751519.sdkstatcache -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/swift-overrides.hmap -Xcc -iquote -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-generated-files.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-own-target-headers.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-all-target-headers.hmap -Xcc -iquote -Xcc /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/ReadWriteLock-project-headers.hmap -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Products/Debug/include -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources-normal/x86_64 -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources/x86_64 -Xcc -I/Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/DerivedSources -Xcc -DDEBUG=1 -module-name ReadWriteLock -frontend-parseable-output -disable-clang-spi -target-sdk-version 15.4 -target-sdk-name macosx15.4 -external-plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib/swift/host/plugins#/Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -external-plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/local/lib/swift/host/plugins#/Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/bin/swift-plugin-server -in-process-plugin-server-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/libSwiftInProcPluginServer.dylib -plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/plugins -plugin-path /Users/ibex/Desktop/Xcode/Xcode-16.3/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/local/lib/swift/host/plugins -o /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Build/Intermediates.noindex/ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.o -index-unit-output-path /ReadWriteLock.build/Debug/ReadWriteLock.build/Objects-normal/x86_64/Driver.o -index-store-path /Users/ibex/Library/Developer/Xcode/DerivedData/2025-frwnwmxeuitojwdrjcckvffwobwc/Index.noindex/DataStore -i

Driver

@main
enum Driver {
    static func main () async throws {
        let hibernate = {
            try await Task.sleep (until: .now + .milliseconds (Int.random (in: 3...7)))
        }
        
        class Counter {
            var value: Int = 0
        }
        
        let u = RWLock ()
        let counter = Counter ()

        Task {
            while true {
                let v = await u.withReadLock {
                    counter.value
                }
                print ("-->", v)
                try await hibernate ()
            }
        }
        
        Task {
            while true {
                let _ = await u.withWriteLock {
                    counter.value += 1
                }
                try await hibernate ()
            }
        }
        
        print ("Enter Control-D to exit.")
        let _ = readLine()
        print ("Finished")
    }
}