Skip to content

Commit 1a25ee0

Browse files
committed
1.1.0 Release
- Added `withLock` and `withTryLock` methods to `ThreadSafeSemaphore` property decorator. - Added Unit Tests for the above - Added examples for the above in the README.MD file.
1 parent de329bd commit 1a25ee0

File tree

4 files changed

+121
-8
lines changed

4 files changed

+121
-8
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,4 @@ fastlane/test_output
8989

9090
iOSInjectionProject/
9191
.DS_Store
92+
README.md

README.md

+56-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ let package = Package(
2222
dependencies: [
2323
.package(
2424
url: "https://github.com/Flowduino/ThreadSafeSwift.git",
25-
.upToNextMajor(from: "1.0.0")
25+
.upToNextMajor(from: "1.1.0")
2626
),
2727
],
2828
//...
@@ -49,7 +49,7 @@ You can then do `import ThreadSafeSwift` in any code that requires it.
4949

5050
Here are some quick and easy usage examples for the features provided by `ThreadSafeSwift`:
5151

52-
### ThreadSafeSemaphore - Property Wrapper
52+
### `@ThreadSafeSemaphore` - Property Wrapper
5353
You can use the `ThreadSafeSemaphore` Property Wrapper to encapsulate any Value Type behind a Thread-Safe `DispatchSemaphore`.
5454
This is extremely easy for most types:
5555
```swift
@@ -84,6 +84,60 @@ func incrementEveryIntegerByOne() {
8484
myInts = values // This would marshal the `DispatchSempahore` and replace the entire Array with our modified one, then release the `DispatchSemaphore`
8585
}
8686
```
87+
88+
### `ThreadSafeSemaphore.withLock` - Execute a Closure while retaining the Lock
89+
Often, it is necessary to perform more than one operation on a Value... and when you need to do this, you'll want to ensure that you retain the `DispatchSemaphore` lock against the value for the duration of these operations.
90+
To facilitate this, we can use the `withLock` method against any `ThreadSafeSemaphore` decorated variable:
91+
```swift
92+
@ThreadSafeSemaphore var myInts: [Int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
93+
```
94+
Here we have a `ThreadSafeSemaphore` decorated Array of Integers.
95+
96+
If we want to perform any number of operations against any number of values within this Array, we can now do so in a thread-safe manner using `withLock`:
97+
```swift
98+
func incrementEachValueByOne() {
99+
_myInts.withLock { value in
100+
for (index, val) in value.enumerated() {
101+
value[index] = val + 1
102+
}
103+
}
104+
}
105+
```
106+
Please pay attention to the preceeding underscore `_` before `myInts` when invoking the `withLock` method. This is important, as the underscore instructs Swift to reference the Property Decorator rather than its `wrappedValue`.
107+
108+
**IMPORTANT NOTE:** - You must *not* reference the variable itself (in the above example, `myInts`) within the scope of the Closure. If you do, the Thread will lock at that command and proceed no further. All mutations to the value must be performed against `value` as defined within the scope of the Closure itself (as shown above).
109+
110+
So, as you can see, we can now encapsulate *complex types* with the `@ThreadSafeSemaphore` decorator and operate against all of its members within the safety of the `DispatchSemaphore` lock.
111+
112+
### `ThreadSafeSemaphore.withTryLock` - Execute a Closure while retaining the Lock IF we can acquire it, otherwise execute a failure Closure
113+
As with `ThreadSafeSemaphore.withLock` (explained above), we may need to perform one or more operations within the context of the `DispatchSemaphore` only *if* it is possible to obtain the `DispatchSemaphore` Lock at that time. Where it is not possible to acquire the `DispatchSemaphore` lock at that moment, we may want to execute another piece of conditional code.
114+
115+
We can do that easily:
116+
```swift
117+
@ThreadSafeSemaphore var myInts: [Int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
118+
```
119+
Again, we declare our `@ThreadSafeSemaphore` decorated Variable.
120+
121+
Now let's see how we would use `withTryLock` against `myInts`:
122+
```swift
123+
func incrementEachValueByOne() {
124+
_myInts.withTryLock { value in
125+
// If we got the Lock
126+
for (index, val) in value.enumerated() {
127+
value[index] = val + 1
128+
}
129+
} _: {
130+
// If we couldn't get the Lock
131+
print("We wanted to acquire the Lock, but couldn't... so we can do something else instead!")
132+
}
133+
}
134+
```
135+
**IMPORTANT NOTE:** - You must *not* reference the variable itself (in the above example, `myInts`) within the scope of the *either* Closure. If you do, the Thread will lock at that command and proceed no further. All mutations to the value must be performed against `value` as defined within the scope of the Closure itself (as shown above).
136+
137+
These *Conditional Closures* are extremely useful where your code needs to progress down a different execution path depending on whether it can or cannot acquire the `DispatchSemaphore` lock at the point of execution.
138+
139+
**TIP:** - I use this very approach to implement "Revolving Door Locks" for Collections. A feature that will be added to this library very soon!
140+
87141
## License
88142

89143
`ThreadSafeSwift` is available under the MIT license. See the [LICENSE file](./LICENSE) for more info.

Sources/ThreadSafeSwift/ThreadSafeSemaphore.swift

+28-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
/**
1111
Enforces a `DispatchSemaphore` Lock against the given Value Type to ensure single-threaded access.
1212
- Author: Simon J. Stuart
13-
- Version: 1.0
13+
- Version: 1.1.0
1414
*/
1515
@propertyWrapper
1616
public struct ThreadSafeSemaphore<T> {
@@ -31,6 +31,33 @@ public struct ThreadSafeSemaphore<T> {
3131
}
3232
}
3333

34+
/**
35+
Invokes your Closure within the confines of the `DispatchSemaphore` Lock (ensuring Mutually-Exclusive Access to your Value for the duration of execution)
36+
- Parameters:
37+
- code: The code you want to execute while the Lock is engaged.
38+
*/
39+
mutating public func withLock(_ code: @escaping (_ value: inout T) -> ()) {
40+
lock.wait()
41+
code(&value)
42+
lock.signal()
43+
}
44+
45+
/**
46+
Attempts to engage the `DispatchSemaphore` and, if successful, will invoke the Closure you provide to the `onLock` Closure. Otherwise, invokes the code you provide to the `onCannotLock` Closure.
47+
- Parameters:
48+
- onLock: The code to execute if the `DispatchSemaphore` can be acquired for mutually-exclusive access.
49+
- onCannotLock: The code to execute if the `DispatchSemaphore` is currently locked by another Thread.
50+
*/
51+
mutating public func withTryLock(_ onLock: @escaping (_ value: inout T) -> (), _ onCannotLock: @escaping ()->()) {
52+
if lock.wait(timeout: DispatchTime.now()) == .success { // If we are able to acquire the lock...
53+
onLock(&value) // Invoke the closure within the lock
54+
lock.signal() // Release the Lock
55+
}
56+
else { // If we can't acquire the lock...
57+
onCannotLock() // Invoke the closure given to execute when the Lock is unavailable
58+
}
59+
}
60+
3461
public init(wrappedValue: T) {
3562
lock.wait()
3663
value = wrappedValue

Tests/ThreadSafeSwiftTests/ThreadSafeSwiftTests.swift

+36-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,41 @@ import XCTest
22
@testable import ThreadSafeSwift
33

44
final class ThreadSafeSwiftTests: XCTestCase {
5-
func testExample() throws {
6-
// This is an example of a functional test case.
7-
// Use XCTAssert and related functions to verify your tests produce the correct
8-
// results.
9-
// XCTAssertEqual(ThreadSafeSwift().text, "Hello, World!")
5+
func testWithLock() throws {
6+
@ThreadSafeSemaphore var myInts: [Int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7+
8+
// Increment every Value by 1
9+
_myInts.withLock { value in
10+
for (index, val) in value.enumerated() {
11+
value[index] = val + 1
12+
}
13+
}
14+
// Assert that each Value has been incremented by 1
15+
_myInts.withLock { value in
16+
for (index, val) in value.enumerated() {
17+
XCTAssertEqual(index + 1, val, "Value \(index) should be \(index + 1) but is \(val)")
18+
}
19+
}
20+
}
21+
22+
func testTryWithLock() throws {
23+
@ThreadSafeSemaphore var myInts: [Int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
24+
25+
_myInts.withTryLock { value in
26+
// If we got the Lock
27+
for (index, val) in value.enumerated() {
28+
value[index] = val + 1
29+
}
30+
} _: {
31+
// If we couldn't get the Lock
32+
XCTFail("We SHOULD have been able to acquire the Lock, but couldn't")
33+
}
34+
35+
// Assert that each Value has been incremented by 1
36+
_myInts.withLock { value in
37+
for (index, val) in value.enumerated() {
38+
XCTAssertEqual(index + 1, val, "Value \(index) should be \(index + 1) but is \(val)")
39+
}
40+
}
1041
}
1142
}

0 commit comments

Comments
 (0)