basic implementation and unit tests

This commit is contained in:
2025-09-14 01:20:52 +01:00
parent 633ab74680
commit 08c65b688e
3 changed files with 208 additions and 0 deletions

26
Package.swift Normal file
View File

@@ -0,0 +1,26 @@
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ThreadSafe",
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "ThreadSafe",
targets: ["ThreadSafe"]
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "ThreadSafe"
),
.testTarget(
name: "ThreadSafeTests",
dependencies: ["ThreadSafe"]
),
]
)

View File

@@ -0,0 +1,114 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
import Foundation
@propertyWrapper
public struct ThreadSafe<T> {
public var wrappedValue: T {
get {
lock.withReadLock { box.value }
}
set {
lock.withWriteLock {
if valueTypeCopyOnWrite {
let addrHash = withUnsafePointer(to: &self) { $0.hashValue }
if addrHash != lastAddressHash {
// addrHash changed - this is a copy
// make new box with new value
box = Box(newValue)
lastAddressHash = addrHash
// new lock for the copied property wrapper
lock = RWLock()
} else {
box.value = newValue
}
} else {
box.value = newValue
}
}
}
}
/***
Regarding `valueTypeCopyOnWrite`:
When `valueTypeCopyOnWrite` is `true` and the wrapped value type is a value type,\
the property wrapper will do extra memory check on writes to determine if the current instance is under a copied struct,\
and if so it will create a new box to ensure the original instance is not affected.\
For reference types `valueTypeCopyOnWrite` does nothing.\
You can disable it if you can be sure that maintaining value type copy isn't necessary, this will give you a back a tiny bit of performance as it won't need to check its memory address on writes anymore
```
struct Example {
// valueTypeCopyOnWrite enabled by default
@ThreadSafe var string: String = "Hello"
// valueTypeCopyOnWrite disabled
@ThreadSafe(valueTypeCopyOnWrite: false) var anotherString: String = "Hello"
// won't do anything as NSMutableString is a class
@ThreadSafe(valueTypeCopyOnWrite: true) var nsString: NSMutableString = "Hello"
}
let example = Example()
var copy = example
copy.string.append(", World!")
print(example.string) // Hello
print(copy.string) // Hello, World!
copy.anotherString.append(", World!")
print(example.anotherString) // Hello, World!
print(copy.anotherString) // Hello, World!
copy.nsString.append(", World!")
print(example.nsString) // Hello, World!
print(copy.nsString) // Hello, World!
```
*/
public init(wrappedValue: T, valueTypeCopyOnWrite: Bool = true) {
self.box = Box(wrappedValue)
self.valueTypeCopyOnWrite = valueTypeCopyOnWrite && !(T.self is AnyObject.Type)
if self.valueTypeCopyOnWrite {
lastAddressHash = withUnsafePointer(to: &self) { $0.hashValue }
}
}
private final class Box {
var value: T
init(_ value: T) { self.value = value }
}
private var box: Box
private var lock = RWLock()
private let valueTypeCopyOnWrite: Bool
private var lastAddressHash: Int!
}
final class RWLock {
private var rwlock = pthread_rwlock_t()
init() {
// Initialize the read-write lock.
// The second parameter is for attributes, which can be nil for default attributes.
let status = pthread_rwlock_init(&rwlock, nil)
assert(status == 0, "Failed to initialize read-write lock: \(status)")
}
deinit {
let status = pthread_rwlock_destroy(&rwlock)
assert(status == 0, "Failed to destroy read-write lock: \(status)")
}
@discardableResult @inline(__always)
func withReadLock<Result>(_ body: () -> Result) -> Result {
pthread_rwlock_rdlock(&rwlock)
defer { pthread_rwlock_unlock(&rwlock) }
return body()
}
@discardableResult @inline(__always)
func withWriteLock<Result>(_ body: () -> Result) -> Result {
pthread_rwlock_wrlock(&rwlock)
defer { pthread_rwlock_unlock(&rwlock) }
return body()
}
}

View File

@@ -0,0 +1,68 @@
import Testing
import Foundation
@testable import ThreadSafe
class ThreadSafetyTest: @unchecked Sendable {
class Obj {
let id = UUID().uuidString
}
// If @ThreadSafe is removed, the test would crash instantly
@ThreadSafe var obj = Obj()
@Test func testThreadSafe() async throws {
let coreCount = ProcessInfo.processInfo.activeProcessorCount
for _ in 0 ..< coreCount / 2 {
Thread { // write thread
while true {
self.obj = .init()
}
}.start()
}
for _ in 0 ..< coreCount / 2{
Thread { // read thread
while self.obj.id.count != 0 {}
}.start()
}
// If it doesn't crash after 10s, consider it passed.
// I think it's good enough as the possibility for a false positive is microscopic.
// Can't think of a way to definitively prove it's thread safe
try? await Task.sleep(for: .seconds(10))
}
}
@Test func TestCopyBehaviour() async throws {
struct Example {
// valueTypeCopyOnWrite enabled by default
@ThreadSafe var string: String = "Hello"
// valueTypeCopyOnWrite disabled
@ThreadSafe(valueTypeCopyOnWrite: false) var anotherString: String = "Hello"
// won't do anything as NSMutableString is a class
@ThreadSafe(valueTypeCopyOnWrite: true) var nsString: NSMutableString = "Hello"
// same as above
@ThreadSafe(valueTypeCopyOnWrite: false) var anotherNSString: NSMutableString = "Hello"
}
let original = Example()
var copy = original
copy.string.append(", World!")
#expect(original.string == "Hello") // copy on write triggered, original not affected
#expect(copy.string == "Hello, World!")
copy.anotherString.append(", World!")
#expect(original.anotherString == "Hello, World!") // valueTypeCopyOnWrite == false, underlying box not copied
#expect(copy.anotherString == "Hello, World!")
copy.nsString.append(", World!")
#expect(original.nsString == "Hello, World!")
#expect(copy.nsString == "Hello, World!")
copy.anotherNSString.append(", World!")
#expect(original.anotherNSString == "Hello, World!")
#expect(copy.anotherNSString == "Hello, World!")
}