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