basic implementation and unit tests
This commit is contained in:
26
Package.swift
Normal file
26
Package.swift
Normal 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"]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
114
Sources/ThreadSafe/ThreadSafe.swift
Normal file
114
Sources/ThreadSafe/ThreadSafe.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Tests/ThreadSafeTests/ThreadSafeTests.swift
Normal file
68
Tests/ThreadSafeTests/ThreadSafeTests.swift
Normal 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!")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user