From cb249fccadab6c27cfbead1c9327576a7dfc7648 Mon Sep 17 00:00:00 2001 From: Anthony Drendel Date: Sat, 14 Feb 2026 18:41:16 +0100 Subject: [PATCH 1/4] Simplify LanguageModelSession state management - Fix race condition when streaming responses. --- .../LanguageModelSession.swift | 79 ++++++++----------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/Sources/AnyLanguageModel/LanguageModelSession.swift b/Sources/AnyLanguageModel/LanguageModelSession.swift index 1b569491..7a89b182 100644 --- a/Sources/AnyLanguageModel/LanguageModelSession.swift +++ b/Sources/AnyLanguageModel/LanguageModelSession.swift @@ -1,10 +1,12 @@ import Foundation import Observation +import os @Observable public final class LanguageModelSession: @unchecked Sendable { - public private(set) var isResponding: Bool = false - public private(set) var transcript: Transcript + public var isResponding: Bool { state.withLock { $0.isResponding } } + public var transcript: Transcript { state.withLock { $0.transcript } } + @ObservationIgnored private let state: OSAllocatedUnfairLock private let model: any LanguageModel public let tools: [any Tool] @@ -20,8 +22,6 @@ public final class LanguageModelSession: @unchecked Sendable { /// with the Foundation Models framework. @ObservationIgnored public var toolExecutionDelegate: (any ToolExecutionDelegate)? - @ObservationIgnored private let respondingState = RespondingState() - public convenience init( model: any LanguageModel, tools: [any Tool] = [], @@ -84,37 +84,29 @@ public final class LanguageModelSession: @unchecked Sendable { } } - self.transcript = finalTranscript + self.state = .init(initialState: .init(finalTranscript)) } public func prewarm(promptPrefix: Prompt? = nil) { model.prewarm(for: self, promptPrefix: promptPrefix) } - nonisolated private func beginResponding() async { - let count = await respondingState.increment() - let active = count > 0 - await MainActor.run { - self.isResponding = active - } + nonisolated private func beginResponding() { + state.withLock { $0.beginResponding() } } - nonisolated private func endResponding() async { - let count = await respondingState.decrement() - let active = count > 0 - await MainActor.run { - self.isResponding = active - } + nonisolated private func endResponding() { + state.withLock { $0.endResponding() } } nonisolated private func wrapRespond(_ operation: () async throws -> T) async throws -> T { - await beginResponding() + beginResponding() do { let result = try await operation() - await endResponding() + endResponding() return result } catch { - await endResponding() + endResponding() throw error } } @@ -127,7 +119,7 @@ public final class LanguageModelSession: @unchecked Sendable { let relay = AsyncThrowingStream.Snapshot, any Error> { continuation in let stream = upstream Task { - await session.beginResponding() + session.beginResponding() var lastSnapshot: ResponseStream.Snapshot? do { for try await snapshot in stream { @@ -152,14 +144,12 @@ public final class LanguageModelSession: @unchecked Sendable { segments: [.text(.init(content: textContent))] ) ) - await MainActor.run { - session.transcript.append(responseEntry) - } + session.state.withLock { $0.transcript.append(responseEntry) } } } catch { continuation.finish(throwing: error) } - await session.endResponding() + session.endResponding() } } return ResponseStream(stream: relay) @@ -202,9 +192,7 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - await MainActor.run { - self.transcript.append(promptEntry) - } + state.withLock { $0.transcript.append(promptEntry) } let response = try await model.respond( within: self, @@ -230,9 +218,9 @@ public final class LanguageModelSession: @unchecked Sendable { ) // Add tool entries and response to transcript - await MainActor.run { - self.transcript.append(contentsOf: response.transcriptEntries) - self.transcript.append(responseEntry) + state.withLock { state in + state.transcript.append(contentsOf: response.transcriptEntries) + state.transcript.append(responseEntry) } return response @@ -253,7 +241,7 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - transcript.append(promptEntry) + state.withLock { $0.transcript.append(promptEntry) } return wrapStream( model.streamResponse( @@ -547,9 +535,7 @@ extension LanguageModelSession { responseFormat: nil ) ) - await MainActor.run { - self.transcript.append(promptEntry) - } + state.withLock { $0.transcript.append(promptEntry) } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) @@ -578,9 +564,9 @@ extension LanguageModelSession { ) // Add tool entries and response to transcript - await MainActor.run { - self.transcript.append(contentsOf: response.transcriptEntries) - self.transcript.append(responseEntry) + state.withLock { state in + state.transcript.append(contentsOf: response.transcriptEntries) + state.transcript.append(responseEntry) } return response @@ -651,7 +637,7 @@ extension LanguageModelSession { responseFormat: nil ) ) - transcript.append(promptEntry) + state.withLock { $0.transcript.append(promptEntry) } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) @@ -902,16 +888,21 @@ private enum ResponseStreamError: Error { // MARK: - -private actor RespondingState { +private struct State: Equatable, Sendable { + var transcript: Transcript + + var isResponding: Bool { count > 0 } private var count = 0 - func increment() -> Int { + init(_ transcript: Transcript) { + self.transcript = transcript + } + + mutating func beginResponding() { count += 1 - return count } - func decrement() -> Int { + mutating func endResponding() { count = max(0, count - 1) - return count } } From 68328a747693f5c8831f124d7a88ccb4e05df802 Mon Sep 17 00:00:00 2001 From: Anthony Drendel Date: Sat, 14 Feb 2026 22:37:50 +0100 Subject: [PATCH 2/4] Replace OSAllocatedUnfairLock with Locked --- .../LanguageModelSession.swift | 27 ++-- Sources/AnyLanguageModel/Locked.swift | 81 ++++++++++ Tests/AnyLanguageModelTests/LockedTests.swift | 152 ++++++++++++++++++ 3 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 Sources/AnyLanguageModel/Locked.swift create mode 100644 Tests/AnyLanguageModelTests/LockedTests.swift diff --git a/Sources/AnyLanguageModel/LanguageModelSession.swift b/Sources/AnyLanguageModel/LanguageModelSession.swift index 7a89b182..b8187279 100644 --- a/Sources/AnyLanguageModel/LanguageModelSession.swift +++ b/Sources/AnyLanguageModel/LanguageModelSession.swift @@ -1,12 +1,11 @@ import Foundation import Observation -import os @Observable public final class LanguageModelSession: @unchecked Sendable { - public var isResponding: Bool { state.withLock { $0.isResponding } } - public var transcript: Transcript { state.withLock { $0.transcript } } - @ObservationIgnored private let state: OSAllocatedUnfairLock + public var isResponding: Bool { state.access { $0.isResponding } } + public var transcript: Transcript { state.access { $0.transcript } } + @ObservationIgnored private let state: Locked private let model: any LanguageModel public let tools: [any Tool] @@ -84,7 +83,7 @@ public final class LanguageModelSession: @unchecked Sendable { } } - self.state = .init(initialState: .init(finalTranscript)) + self.state = .init(.init(finalTranscript)) } public func prewarm(promptPrefix: Prompt? = nil) { @@ -92,11 +91,11 @@ public final class LanguageModelSession: @unchecked Sendable { } nonisolated private func beginResponding() { - state.withLock { $0.beginResponding() } + state.access { $0.beginResponding() } } nonisolated private func endResponding() { - state.withLock { $0.endResponding() } + state.access { $0.endResponding() } } nonisolated private func wrapRespond(_ operation: () async throws -> T) async throws -> T { @@ -144,7 +143,7 @@ public final class LanguageModelSession: @unchecked Sendable { segments: [.text(.init(content: textContent))] ) ) - session.state.withLock { $0.transcript.append(responseEntry) } + session.state.access { $0.transcript.append(responseEntry) } } } catch { continuation.finish(throwing: error) @@ -192,7 +191,7 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - state.withLock { $0.transcript.append(promptEntry) } + state.access { $0.transcript.append(promptEntry) } let response = try await model.respond( within: self, @@ -218,7 +217,7 @@ public final class LanguageModelSession: @unchecked Sendable { ) // Add tool entries and response to transcript - state.withLock { state in + state.access { state in state.transcript.append(contentsOf: response.transcriptEntries) state.transcript.append(responseEntry) } @@ -241,7 +240,7 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - state.withLock { $0.transcript.append(promptEntry) } + state.access { $0.transcript.append(promptEntry) } return wrapStream( model.streamResponse( @@ -535,7 +534,7 @@ extension LanguageModelSession { responseFormat: nil ) ) - state.withLock { $0.transcript.append(promptEntry) } + state.access { $0.transcript.append(promptEntry) } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) @@ -564,7 +563,7 @@ extension LanguageModelSession { ) // Add tool entries and response to transcript - state.withLock { state in + state.access { state in state.transcript.append(contentsOf: response.transcriptEntries) state.transcript.append(responseEntry) } @@ -637,7 +636,7 @@ extension LanguageModelSession { responseFormat: nil ) ) - state.withLock { $0.transcript.append(promptEntry) } + state.access { $0.transcript.append(promptEntry) } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) diff --git a/Sources/AnyLanguageModel/Locked.swift b/Sources/AnyLanguageModel/Locked.swift new file mode 100644 index 00000000..111a54ee --- /dev/null +++ b/Sources/AnyLanguageModel/Locked.swift @@ -0,0 +1,81 @@ +#if canImport(Darwin) + import Darwin + private typealias PlatformLock = os_unfair_lock +#elseif canImport(Glibc) + import Glibc + #if os(FreeBSD) || os(OpenBSD) + private typealias PlatformLock = pthread_mutex_t? + #else + private typealias PlatformLock = pthread_mutex_t + #endif +#else + #error("Unsupported platform") +#endif + +struct Locked { + private final class Buffer: ManagedBuffer { + deinit { withUnsafeMutablePointerToElements { $0.destroy() } } + } + + private let buffer: ManagedBuffer + + init(_ state: State) { + buffer = Buffer.create(minimumCapacity: 1) { buffer in + buffer.withUnsafeMutablePointerToElements { PlatformLockPointer.initialize($0) } + return state + } + } + + func access(_ block: (inout State) throws -> T) rethrows -> T { + try buffer.withUnsafeMutablePointers { header, lock in + lock.lock() + defer { lock.unlock() } + return try block(&header.pointee) + } + } +} + +extension Locked: @unchecked Sendable where State: Sendable {} + +private typealias PlatformLockPointer = UnsafeMutablePointer +private extension PlatformLockPointer { + static func initialize(_ pointer: PlatformLockPointer) { + #if canImport(Darwin) + pointer.initialize(to: os_unfair_lock()) + #elseif canImport(Glibc) + let result = pthread_mutex_init(pointer, nil) + precondition(result == 0, "pthread_mutex_init failed") + #else + #error("Unsupported platform") + #endif + } + + func destroy() { + #if canImport(Glibc) + let result = pthread_mutex_destroy(self) + precondition(result == 0, "pthread_mutex_destroy failed") + #endif + deinitialize(count: 1) + } + + func lock() { + #if canImport(Darwin) + os_unfair_lock_lock(self) + #elseif canImport(Glibc) + pthread_mutex_lock(self) + #else + #error("Unsupported platform") + #endif + } + + func unlock() { + #if canImport(Darwin) + os_unfair_lock_unlock(self) + #elseif canImport(Glibc) + let result = pthread_mutex_unlock(self) + precondition(result == 0, "pthread_mutex_unlock failed") + #else + #error("Unsupported platform") + #endif + } +} diff --git a/Tests/AnyLanguageModelTests/LockedTests.swift b/Tests/AnyLanguageModelTests/LockedTests.swift new file mode 100644 index 00000000..b81eda9e --- /dev/null +++ b/Tests/AnyLanguageModelTests/LockedTests.swift @@ -0,0 +1,152 @@ +import Foundation +import Testing + +@testable import AnyLanguageModel + +@Suite("Locked") +struct LockedTests { + @Test("Read access returns the initial value") + func readAccess() { + let locked = Locked(42) + let value = locked.access { $0 } + #expect(value == 42) + } + + @Test("Write access mutates the state") + func writeAccess() { + let locked = Locked(0) + locked.access { $0 = 99 } + let value = locked.access { $0 } + #expect(value == 99) + } + + @Test("Access returns the value from the closure") + func returnValue() { + let locked = Locked("hello") + let result = locked.access { state -> Int in + state += " world" + return state.count + } + #expect(result == 11) + #expect(locked.access { $0 } == "hello world") + } + + @Test("Access propagates thrown errors") + func throwsPropagation() { + struct TestError: Error, Equatable {} + + let locked = Locked(0) + #expect(throws: TestError.self) { + try locked.access { _ in throw TestError() } + } + } + + @Test("Works with complex state types") + func complexState() { + struct State { + var name: String + var count: Int + var tags: [String] + } + + let locked = Locked(State(name: "initial", count: 0, tags: [])) + locked.access { state in + state.name = "updated" + state.count = 5 + state.tags.append("a") + state.tags.append("b") + } + + let snapshot = locked.access { $0 } + #expect(snapshot.name == "updated") + #expect(snapshot.count == 5) + #expect(snapshot.tags == ["a", "b"]) + } + + @Test("Concurrent increments produce the correct final count") + func concurrentCounter() async { + let locked = Locked(0) + let iterations = 100_000 + + await withTaskGroup(of: Void.self) { group in + for _ in 0 ..< iterations { + group.addTask { locked.access { $0 += 1 } } + } + } + + let finalValue = locked.access { $0 } + #expect(finalValue == iterations) + } + + @Test("Concurrent increments with mixed task priorities") + func concurrentCounterWithMixedPriorities() async { + let locked = Locked(0) + let iterations = 100_000 + + await withTaskGroup(of: Void.self) { group in + for i in 0 ..< iterations { + let priority: TaskPriority = i.isMultiple(of: 2) ? .high : .background + group.addTask(priority: priority) { + locked.access { $0 += 1 } + } + } + } + + let finalValue = locked.access { $0 } + #expect(finalValue == iterations) + } + + @Test("Concurrent appends to an array") + func concurrentArrayAppend() async { + let locked = Locked<[Int]>([]) + let iterations = 10_000 + + await withTaskGroup(of: Void.self) { group in + for i in 0 ..< iterations { + group.addTask { locked.access { $0.append(i) } } + } + } + + let finalArray = locked.access { $0 } + #expect(finalArray.count == iterations) + } + + @Test("Multiple independent locked values mutated concurrently") + func multipleLockedValues() async { + let lockedA = Locked(0) + let lockedB = Locked(0) + let iterations = 50_000 + + await withTaskGroup(of: Void.self) { group in + for _ in 0 ..< iterations { + group.addTask { lockedA.access { $0 += 1 } } + group.addTask { lockedB.access { $0 += 1 } } + } + } + + #expect(lockedA.access { $0 } == iterations) + #expect(lockedB.access { $0 } == iterations) + } + + @Test("Can wrap a non-Sendable type") + func nonSendableElement() { + class Box { + var value: Int + init(_ value: Int) { self.value = value } + } + + let locked = Locked(Box(10)) + locked.access { $0.value += 5 } + let result = locked.access { $0.value } + #expect(result == 15) + } + + @Test("Copies share the same underlying storage") + func copySharesStorage() { + let original = Locked(0) + let copy = original + original.access { $0 = 42 } + let value = copy.access { $0 } + #expect(value == 42) + } +} From 0967c49256b799649d9aff7a1d79a05fd7404126 Mon Sep 17 00:00:00 2001 From: Anthony Drendel Date: Sun, 15 Feb 2026 00:41:58 +0100 Subject: [PATCH 3/4] Re-enable observation for LanguageModelSession.isResponding and transcript --- .../LanguageModelSession.swift | 56 ++++++++--- .../ObservationTests.swift | 95 +++++++++++++++++++ 2 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 Tests/AnyLanguageModelTests/ObservationTests.swift diff --git a/Sources/AnyLanguageModel/LanguageModelSession.swift b/Sources/AnyLanguageModel/LanguageModelSession.swift index b8187279..1c00de75 100644 --- a/Sources/AnyLanguageModel/LanguageModelSession.swift +++ b/Sources/AnyLanguageModel/LanguageModelSession.swift @@ -3,8 +3,16 @@ import Observation @Observable public final class LanguageModelSession: @unchecked Sendable { - public var isResponding: Bool { state.access { $0.isResponding } } - public var transcript: Transcript { state.access { $0.transcript } } + public var isResponding: Bool { + access(keyPath: \.isResponding) + return state.access { $0.isResponding } + } + + public var transcript: Transcript { + access(keyPath: \.transcript) + return state.access { $0.transcript } + } + @ObservationIgnored private let state: Locked private let model: any LanguageModel @@ -91,11 +99,15 @@ public final class LanguageModelSession: @unchecked Sendable { } nonisolated private func beginResponding() { - state.access { $0.beginResponding() } + withMutation(keyPath: \.isResponding) { + state.access { $0.beginResponding() } + } } nonisolated private func endResponding() { - state.access { $0.endResponding() } + withMutation(keyPath: \.isResponding) { + state.access { $0.endResponding() } + } } nonisolated private func wrapRespond(_ operation: () async throws -> T) async throws -> T { @@ -143,7 +155,9 @@ public final class LanguageModelSession: @unchecked Sendable { segments: [.text(.init(content: textContent))] ) ) - session.state.access { $0.transcript.append(responseEntry) } + session.withMutation(keyPath: \.transcript) { + session.state.access { $0.transcript.append(responseEntry) } + } } } catch { continuation.finish(throwing: error) @@ -191,7 +205,9 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - state.access { $0.transcript.append(promptEntry) } + withMutation(keyPath: \.transcript) { + state.access { $0.transcript.append(promptEntry) } + } let response = try await model.respond( within: self, @@ -217,9 +233,11 @@ public final class LanguageModelSession: @unchecked Sendable { ) // Add tool entries and response to transcript - state.access { state in - state.transcript.append(contentsOf: response.transcriptEntries) - state.transcript.append(responseEntry) + withMutation(keyPath: \.transcript) { + state.access { state in + state.transcript.append(contentsOf: response.transcriptEntries) + state.transcript.append(responseEntry) + } } return response @@ -240,7 +258,9 @@ public final class LanguageModelSession: @unchecked Sendable { responseFormat: nil ) ) - state.access { $0.transcript.append(promptEntry) } + withMutation(keyPath: \.transcript) { + state.access { $0.transcript.append(promptEntry) } + } return wrapStream( model.streamResponse( @@ -534,7 +554,9 @@ extension LanguageModelSession { responseFormat: nil ) ) - state.access { $0.transcript.append(promptEntry) } + withMutation(keyPath: \.transcript) { + state.access { $0.transcript.append(promptEntry) } + } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) @@ -563,9 +585,11 @@ extension LanguageModelSession { ) // Add tool entries and response to transcript - state.access { state in - state.transcript.append(contentsOf: response.transcriptEntries) - state.transcript.append(responseEntry) + withMutation(keyPath: \.transcript) { + state.access { state in + state.transcript.append(contentsOf: response.transcriptEntries) + state.transcript.append(responseEntry) + } } return response @@ -636,7 +660,9 @@ extension LanguageModelSession { responseFormat: nil ) ) - state.access { $0.transcript.append(promptEntry) } + withMutation(keyPath: \.transcript) { + state.access { $0.transcript.append(promptEntry) } + } // Extract text content for the Prompt parameter let textPrompt = Prompt(prompt) diff --git a/Tests/AnyLanguageModelTests/ObservationTests.swift b/Tests/AnyLanguageModelTests/ObservationTests.swift new file mode 100644 index 00000000..470a65ac --- /dev/null +++ b/Tests/AnyLanguageModelTests/ObservationTests.swift @@ -0,0 +1,95 @@ +import Observation +import Testing + +@testable import AnyLanguageModel + +@Suite("Observation") +struct ObservationTests { + @Test("Tracking transcript fires onChange when respond mutates it") + func transcriptObservationTriggeredByRespond() async throws { + let session = LanguageModelSession(model: MockLanguageModel.fixed("Hello")) + let changed = Locked(false) + + withObservationTracking { + _ = session.transcript + } onChange: { + changed.access { $0 = true } + } + + try await session.respond(to: "Hi") + #expect(changed.access { $0 } == true) + } + + @Test("Tracking transcript fires onChange when streamResponse mutates it") + func transcriptObservationTriggeredByStreamResponse() async throws { + let session = LanguageModelSession(model: MockLanguageModel.streamingMock()) + let changed = Locked(false) + + withObservationTracking { + _ = session.transcript + } onChange: { + changed.access { $0 = true } + } + + let stream = session.streamResponse(to: "Hi") + for try await _ in stream {} + try await Task.sleep(for: .milliseconds(10)) + + #expect(changed.access { $0 } == true) + } + + @Test("Tracking isResponding fires onChange when respond mutates it") + func isRespondingObservationTriggeredByRespond() async throws { + let session = LanguageModelSession(model: MockLanguageModel.fixed("Hello")) + let changed = Locked(false) + + withObservationTracking { + _ = session.isResponding + } onChange: { + changed.access { $0 = true } + } + + try await session.respond(to: "Hi") + #expect(changed.access { $0 } == true) + } + + @Test("No onChange fires when no properties are tracked") + func noObservationWithoutTrackedRead() async throws { + let session = LanguageModelSession(model: MockLanguageModel.fixed("Hello")) + let changed = Locked(false) + + withObservationTracking { + // Intentionally do not read any session properties + } onChange: { + changed.access { $0 = true } + } + + try await session.respond(to: "Hi") + #expect(changed.access { $0 } == false) + } + + @Test("Re-registering observation tracks subsequent changes") + func observationReregistersAfterChange() async throws { + let session = LanguageModelSession(model: MockLanguageModel.fixed("Hello")) + let changeCount = Locked(0) + + withObservationTracking { + _ = session.transcript + } onChange: { + changeCount.access { $0 += 1 } + } + + try await session.respond(to: "First") + #expect(changeCount.access { $0 } == 1) + + // Re-register after the first change fires + withObservationTracking { + _ = session.transcript + } onChange: { + changeCount.access { $0 += 1 } + } + + try await session.respond(to: "Second") + #expect(changeCount.access { $0 } == 2) + } +} From 3592070aa66e03d12942d871832335883a25f290 Mon Sep 17 00:00:00 2001 From: Anthony Drendel Date: Tue, 17 Feb 2026 13:17:56 +0100 Subject: [PATCH 4/4] Replace os_unfair_lock and pthread_mutex_t with NSLock --- Sources/AnyLanguageModel/Locked.swift | 77 +++------------------------ 1 file changed, 6 insertions(+), 71 deletions(-) diff --git a/Sources/AnyLanguageModel/Locked.swift b/Sources/AnyLanguageModel/Locked.swift index 111a54ee..25d93d46 100644 --- a/Sources/AnyLanguageModel/Locked.swift +++ b/Sources/AnyLanguageModel/Locked.swift @@ -1,81 +1,16 @@ -#if canImport(Darwin) - import Darwin - private typealias PlatformLock = os_unfair_lock -#elseif canImport(Glibc) - import Glibc - #if os(FreeBSD) || os(OpenBSD) - private typealias PlatformLock = pthread_mutex_t? - #else - private typealias PlatformLock = pthread_mutex_t - #endif -#else - #error("Unsupported platform") -#endif +import Foundation -struct Locked { - private final class Buffer: ManagedBuffer { - deinit { withUnsafeMutablePointerToElements { $0.destroy() } } - } - - private let buffer: ManagedBuffer +final class Locked { + private let lock = NSLock() + private var state: State init(_ state: State) { - buffer = Buffer.create(minimumCapacity: 1) { buffer in - buffer.withUnsafeMutablePointerToElements { PlatformLockPointer.initialize($0) } - return state - } + self.state = state } func access(_ block: (inout State) throws -> T) rethrows -> T { - try buffer.withUnsafeMutablePointers { header, lock in - lock.lock() - defer { lock.unlock() } - return try block(&header.pointee) - } + try lock.withLock { try block(&self.state) } } } extension Locked: @unchecked Sendable where State: Sendable {} - -private typealias PlatformLockPointer = UnsafeMutablePointer -private extension PlatformLockPointer { - static func initialize(_ pointer: PlatformLockPointer) { - #if canImport(Darwin) - pointer.initialize(to: os_unfair_lock()) - #elseif canImport(Glibc) - let result = pthread_mutex_init(pointer, nil) - precondition(result == 0, "pthread_mutex_init failed") - #else - #error("Unsupported platform") - #endif - } - - func destroy() { - #if canImport(Glibc) - let result = pthread_mutex_destroy(self) - precondition(result == 0, "pthread_mutex_destroy failed") - #endif - deinitialize(count: 1) - } - - func lock() { - #if canImport(Darwin) - os_unfair_lock_lock(self) - #elseif canImport(Glibc) - pthread_mutex_lock(self) - #else - #error("Unsupported platform") - #endif - } - - func unlock() { - #if canImport(Darwin) - os_unfair_lock_unlock(self) - #elseif canImport(Glibc) - let result = pthread_mutex_unlock(self) - precondition(result == 0, "pthread_mutex_unlock failed") - #else - #error("Unsupported platform") - #endif - } -}