mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
`Atomic` can't be used recursively (due to Swift's `inout` limitation), so we pay an unnecessary price in performance using `NSRecursiveLock` that we don't need. See https://swiftrocks.com/thread-safety-in-swift for a benchmark. The implementation still allows using `NSRecursiveLock` when necessary.
98 lines
2.4 KiB
Markdown
98 lines
2.4 KiB
Markdown
# Thread Safety 👾
|
|
---
|
|
|
|
## What is it?
|
|
|
|
There are several places in the SDK that require synchronization. In other words, the SDK needs to ensure that multiple users can't simultaneously read and write while causing data races.
|
|
|
|
To aid with that task there are several abstractions available:
|
|
|
|
### `Lock`
|
|
|
|
This type is a building block to guarantee such synchronization. Its only method `perform` can be used to do that:
|
|
```swift
|
|
class Data {
|
|
private let lock = Lock()
|
|
|
|
private var calls = 0
|
|
|
|
func threadSafeMethod() -> Int {
|
|
return self.lock.perform {
|
|
self.calls += 1
|
|
return self.calls
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
If you need the lock to be _reentrant_ (so that calling `perform` recursively won't cause deadlocks), you can use `Lock(.recursive)`:
|
|
|
|
```swift
|
|
class Data {
|
|
private let lock = Lock(.recursive)
|
|
|
|
private var calls = 0
|
|
|
|
func threadSafeMethod() -> Int {
|
|
return self.lock.perform {
|
|
self.threadSafeIncrement()
|
|
return self.calls
|
|
}
|
|
}
|
|
|
|
private func threadSafeIncrement() {
|
|
return self.lock.perform {
|
|
self.calls += 1
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### `Atomic`
|
|
|
|
If what needs to be synchronized is a piece of data, this can be more easily accomplished using `Atomic`. It serves the same purpose as `Lock`, but makes handling data in a thread-safe manner easier.
|
|
|
|
It encapsulates a type `T`, and guarantees no read or write to that value can result in race conditions:
|
|
|
|
```swift
|
|
class Data {
|
|
private let calls = Atomic<Int>(0)
|
|
|
|
func threadSafeIncrement() -> Int {
|
|
return self.calls.modify { calls in
|
|
calls += 1
|
|
return calls
|
|
}
|
|
}
|
|
|
|
var threadSafeValue: Int {
|
|
return self.calls.value
|
|
}
|
|
}
|
|
```
|
|
|
|
|
|
### `SynchronizedUserDefaults`
|
|
|
|
Some places in the SDK (`DeviceCache` being one example) need to provide safety around reads and writes to `UserDefaults`.
|
|
|
|
`SynchronizedUserDefaults` can be used in such cases to provide the same guarantees. It's built on top of `Atomic`, so it works in a similar way:
|
|
```swift
|
|
class Data {
|
|
private let userDefaults = SynchronizedUserDefaults(userDefaults: .main)
|
|
|
|
func threadSafeIncrement() {
|
|
self.userDefaults.write {
|
|
let calls = $0.integer(forKey: "key") + 1
|
|
$0.set(calls, forKey: "key")
|
|
}
|
|
}
|
|
|
|
var threadSafeValue: Int {
|
|
return self.userDefaults.read {
|
|
$0.integer(forKey: "key")
|
|
}
|
|
}
|
|
}
|
|
```
|