Skip to content

Commit 29ee0b1

Browse files
committed
feature: add recursive lock
1 parent cddce8e commit 29ee0b1

7 files changed

+268
-53
lines changed

Sources/YUKLock/Lock.swift

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Lock.swift
3+
// YUKLock
4+
//
5+
// Created by Ruslan Lutfullin on 2/7/21.
6+
//
7+
8+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
9+
public protocol Lock: AnyObject {
10+
@inlinable func trySync<R>(_ block: () throws -> R) rethrows -> R?
11+
@inlinable func sync<R>(_ block: () throws -> R) rethrows -> R
12+
//
13+
@inlinable func locked() -> Bool
14+
//
15+
@inlinable func lock()
16+
@inlinable func unlock()
17+
//
18+
init()
19+
}
20+
21+
extension Lock {
22+
@inlinable public func trySync<R>(_ block: () throws -> R) rethrows -> R? {
23+
guard locked() else {
24+
return nil
25+
}
26+
defer {
27+
unlock()
28+
}
29+
return try block()
30+
}
31+
@inlinable public func sync<R>(_ block: () throws -> R) rethrows -> R {
32+
lock()
33+
defer {
34+
unlock()
35+
}
36+
return try block()
37+
}
38+
}

Sources/YUKLock/Locked.swift

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// Locked.swift
3+
// YUKLock
4+
//
5+
// Created by Ruslan Lutfullin on 2/7/21.
6+
//
7+
8+
// MARK: -
9+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
10+
@propertyWrapper
11+
@dynamicMemberLookup
12+
public final class Locked<Value, LockType: Lock> {
13+
// MARK: Internal Props
14+
@usableFromInline internal let lock = LockType()
15+
@usableFromInline internal var value: Value
16+
17+
// MARK: Public Props
18+
public var wrappedValue: Value {
19+
_read {
20+
lock.lock()
21+
defer {
22+
lock.unlock()
23+
}
24+
yield value
25+
}
26+
_modify {
27+
lock.lock()
28+
defer {
29+
lock.unlock()
30+
}
31+
yield &value
32+
}
33+
}
34+
public var projectedValue: Locked { self }
35+
36+
// MARK: Public Methods
37+
@inlinable public func read<T>(_ body: (Value) throws -> T) rethrows -> T? {
38+
try lock.trySync { try body(value) }
39+
}
40+
@inlinable @discardableResult public func write<T>(_ body: (inout Value) throws -> T) rethrows -> T? {
41+
try lock.trySync { try body(&value) }
42+
}
43+
44+
// MARK: Public Subscripts
45+
@inlinable public subscript<Property>(dynamicMember keyPath: KeyPath<Value, Property>) -> Property {
46+
lock.sync { value[keyPath: keyPath] }
47+
}
48+
@inlinable public subscript<Property>(dynamicMember keyPath: WritableKeyPath<Value, Property>) -> Property {
49+
get {
50+
lock.sync { value[keyPath: keyPath] }
51+
}
52+
set {
53+
lock.sync { value[keyPath: keyPath] = newValue }
54+
}
55+
}
56+
@inlinable public subscript<Property>(dynamicMember keyPath: ReferenceWritableKeyPath<Value, Property>) -> Property {
57+
get {
58+
lock.sync { value[keyPath: keyPath] }
59+
}
60+
set {
61+
lock.sync { value[keyPath: keyPath] = newValue }
62+
}
63+
}
64+
65+
// MARK: Public Inits
66+
@inlinable public init(wrappedValue: Value) {
67+
value = wrappedValue
68+
}
69+
}
70+
71+
// MARK: -
72+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
73+
public typealias UnfairLocked<Value> = Locked<Value, UnfairLock>
74+
75+
// MARK: -
76+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
77+
public typealias RecursiveLocked<Value> = Locked<Value, RecursiveLock>

Sources/YUKLock/RecursiveLock.swift

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// RecursiveLock.swift
3+
// YUKLock
4+
//
5+
// Created by Ruslan Lutfullin on 2/7/21.
6+
//
7+
8+
import Darwin.POSIX.pthread
9+
10+
// MARK: -
11+
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
12+
public final class RecursiveLock: Lock {
13+
// MARK: Internal Props
14+
@usableFromInline internal private(set) var _lock: UnsafeMutablePointer<pthread_mutex_t>
15+
16+
// MARK: Public Methods
17+
@inlinable public func locked() -> Bool {
18+
pthread_mutex_trylock(_lock) == .zero
19+
}
20+
//
21+
@inlinable public func lock() {
22+
pthread_mutex_lock(_lock)
23+
}
24+
@inlinable public func unlock() {
25+
pthread_mutex_unlock(_lock)
26+
}
27+
28+
// MARK: Public Inits
29+
public init() {
30+
_lock = .allocate(capacity: 1)
31+
var attr: UnsafeMutablePointer<pthread_mutexattr_t>
32+
attr = .allocate(capacity: 1)
33+
pthread_mutexattr_init(attr)
34+
pthread_mutexattr_settype(attr, PTHREAD_MUTEX_RECURSIVE)
35+
pthread_mutex_init(_lock, attr)
36+
pthread_mutexattr_destroy(attr)
37+
attr.deinitialize(count: 1)
38+
attr.deallocate()
39+
}
40+
deinit {
41+
pthread_mutex_destroy(_lock)
42+
_lock.deinitialize(count: 1)
43+
_lock.deallocate()
44+
}
45+
}

Sources/YUKLock/UnfairLock.swift

+2-35
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,11 @@ import Darwin.os.lock
99

1010
// MARK: -
1111
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
12-
public final class UnfairLock {
12+
public final class UnfairLock: Lock {
1313
// MARK: Internal Props
1414
@usableFromInline internal let _lock: os_unfair_lock_t
1515

1616
// MARK: Public Methods
17-
@inlinable public func precondition(_ precidcate: Predicate) {
18-
if precidcate == .onThreadOwner {
19-
os_unfair_lock_assert_owner(_lock)
20-
}
21-
else {
22-
os_unfair_lock_assert_not_owner(_lock)
23-
}
24-
}
25-
//
26-
@inlinable public func trySync<R>(_ block: () throws -> R) rethrows -> R? {
27-
guard locked() else {
28-
return nil
29-
}
30-
defer {
31-
unlock()
32-
}
33-
return try block()
34-
}
35-
@inlinable public func sync<R>(_ block: () throws -> R) rethrows -> R {
36-
lock()
37-
defer {
38-
unlock()
39-
}
40-
return try block()
41-
}
42-
//
4317
@inlinable public func locked() -> Bool {
4418
os_unfair_lock_trylock(_lock)
4519
}
@@ -54,17 +28,10 @@ public final class UnfairLock {
5428
// MARK: Public Inits
5529
public init() {
5630
_lock = .allocate(capacity: 1)
57-
_lock.initialize(to: os_unfair_lock())
31+
_lock.initialize(to: .init())
5832
}
5933
deinit {
6034
_lock.deinitialize(count: 1)
6135
_lock.deallocate()
6236
}
6337
}
64-
65-
@available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, macCatalyst 13.0, *)
66-
extension UnfairLock {
67-
public enum Predicate {
68-
case onThreadOwner, notOnThreadOwner
69-
}
70-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//
2+
// RecursiveLockTests.swift
3+
// YUKLockTests
4+
//
5+
// Created by Ruslan Lutfullin on 2/7/21.
6+
//
7+
8+
import XCTest
9+
@testable import YUKLock
10+
11+
// MARK: -
12+
final class RecursiveLockTests: XCTestCase {
13+
// MARK: Private Props
14+
private var lock: Lock!
15+
16+
// MARK: Public Static Props
17+
static var allTests = [("testLockUnlock", testLockUnlock),
18+
("testSync", testSync),
19+
("testLocked", testLocked),
20+
("testTrySync", testTrySync)]
21+
22+
// MARK: Public Methods
23+
override func setUp() {
24+
super.setUp()
25+
lock = RecursiveLock()
26+
}
27+
//
28+
func testLockUnlock() {
29+
executeLockTest { (block) in
30+
self.lock.lock()
31+
block()
32+
self.lock.unlock()
33+
}
34+
}
35+
func testSync() {
36+
executeLockTest { (block) in self.lock.sync { block() } }
37+
}
38+
func testLocked() {
39+
lock.lock()
40+
XCTAssertTrue(lock.locked())
41+
lock.unlock()
42+
lock.unlock()
43+
44+
XCTAssertTrue(lock.locked())
45+
lock.unlock()
46+
}
47+
func testTrySync() {
48+
lock.lock()
49+
XCTAssertNotNil(lock.trySync({ }))
50+
lock.unlock()
51+
XCTAssertNotNil(lock.trySync({ }))
52+
}
53+
//
54+
override func tearDown() {
55+
lock = nil
56+
super.tearDown()
57+
}
58+
}
59+
60+
extension RecursiveLockTests {
61+
private func executeLockTest(performBlock: @escaping (_ block: () -> Void) -> Void) {
62+
let dispatchBlockCount = 16
63+
let iterationCountPerBlock = 100_000
64+
let queues: [DispatchQueue] = [.global(qos: .userInteractive),
65+
.global(),
66+
.global(qos: .utility)]
67+
var value = 0
68+
69+
let group = DispatchGroup()
70+
71+
(0..<dispatchBlockCount).forEach {
72+
group.enter()
73+
let queue = queues[$0 % queues.count]
74+
queue.async {
75+
(0..<iterationCountPerBlock).forEach { (_) in
76+
performBlock {
77+
value += 2
78+
value -= 1
79+
}
80+
}
81+
group.leave()
82+
}
83+
}
84+
85+
_ = group.wait(timeout: .distantFuture)
86+
87+
XCTAssertEqual(value, dispatchBlockCount * iterationCountPerBlock)
88+
}
89+
}

Tests/YUKLockTests/YUKLockTests.swift Tests/YUKLockTests/UnfairLockTests.swift

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
//
2+
// UnfairLockTests.swift
3+
// YUKLockTests
4+
//
5+
// Created by Ruslan Lutfullin on 2/7/21.
6+
//
7+
18
import XCTest
29
@testable import YUKLock
310

411
// MARK: -
5-
final class YUKLockTests: XCTestCase {
12+
final class UnfairLockTests: XCTestCase {
613
// MARK: Private Props
714
private var lock: UnfairLock!
815

916
// MARK: Public Static Props
10-
static var allTests = [("testUnfairLock", testLockUnlock),
17+
static var allTests = [("testLockUnlock", testLockUnlock),
1118
("testSync", testSync),
1219
("testLocked", testLocked),
13-
("testTrySync", testTrySync),
14-
("testPrecondition", testPrecondition)]
20+
("testTrySync", testTrySync)]
1521

1622
// MARK: Public Methods
1723
override func setUp() {
@@ -27,38 +33,30 @@ final class YUKLockTests: XCTestCase {
2733
}
2834
}
2935
func testSync() {
30-
executeLockTest { (block) in
31-
self.lock.sync { block() }
32-
}
36+
executeLockTest { (block) in self.lock.sync { block() } }
3337
}
3438
func testLocked() {
3539
lock.lock()
3640
XCTAssertFalse(lock.locked())
3741
lock.unlock()
42+
3843
XCTAssertTrue(lock.locked())
3944
lock.unlock()
4045
}
4146
func testTrySync() {
4247
lock.lock()
43-
XCTAssertNil(lock.trySync({}))
48+
XCTAssertNil(lock.trySync({ }))
4449
lock.unlock()
45-
XCTAssertNotNil(lock.trySync({}))
46-
}
47-
func testPrecondition() {
48-
lock.lock()
49-
lock.precondition(.onThreadOwner)
50-
lock.unlock()
51-
lock.precondition(.notOnThreadOwner)
50+
XCTAssertNotNil(lock.trySync({ }))
5251
}
5352
//
5453
override func tearDown() {
5554
lock = nil
5655
super.tearDown()
5756
}
58-
5957
}
6058

61-
extension YUKLockTests {
59+
extension UnfairLockTests {
6260
private func executeLockTest(performBlock: @escaping (_ block: () -> Void) -> Void) {
6361
let dispatchBlockCount = 16
6462
let iterationCountPerBlock = 100_000

Tests/YUKLockTests/XCTestManifests.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import XCTest
22

33
#if !canImport(ObjectiveC)
44
public func allTests() -> [XCTestCaseEntry] {
5-
[ testCase(YUKLockTests.allTests) ]
5+
[ testCase(UnfairLockTests.allTests),
6+
testcase(RecursiveLockTests.allTests) ]
67
}
78
#endif

0 commit comments

Comments
 (0)