Last active
February 17, 2021 07:15
-
-
Save alexcohn/b5e0a5f8bef6a55a37372fd8842a85d6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class B: Encodable { | |
static let needUpdate = Notification.Name("B") | |
var cnt = 0; | |
// and many, many other things | |
func upload() { // serialize and upload it to the cloud | |
sleep(1000) | |
} | |
} | |
class A1 { // this is not thread-safe | |
private var b = B() | |
private func incrementB() { | |
b.cnt += 1 | |
} | |
func replaceB() { | |
let localB = b | |
b = B() | |
localB.upload() | |
} | |
init() { | |
NotificationCenter.default.addObserver(forName: B.needUpdate, object: nil, queue: OperationQueue.main) { [b] _ in | |
b.cnt += 1 | |
} | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
} | |
class A2 { // this is thread-safe, but `incrementB()` will be stuck while we upload | |
private var b = B() | |
private let lock = NSLock() | |
private func incrementB() { | |
lock.synchronized { | |
b.cnt += 1 | |
} | |
} | |
func replaceB() { | |
lock.synchronized { | |
let localB = b | |
b = B() | |
localB.upload() | |
} | |
} | |
init() { | |
NotificationCenter.default.addObserver(forName: B.needUpdate, object: nil, queue: OperationQueue.main) { [weak self] _ in | |
self?.incrementB() | |
} | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
} | |
class A3 { // now we are not stuck | |
private var b = B() | |
private let lock = NSLock() | |
private func incrementB() { | |
lock.synchronized { | |
b.cnt += 1 | |
} | |
} | |
func replaceB() { | |
var localB: B? = nil | |
lock.synchronized { | |
localB = b | |
b = B() | |
} | |
localB!.upload() | |
} | |
init() { | |
NotificationCenter.default.addObserver(forName: B.needUpdate, object: nil, queue: OperationQueue.main) { [b, lock] _ in | |
lock.synchronized { | |
b.cnt += 1 | |
} | |
} | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@robertmryan, thanks for your questions. I deeply appreciate your help.
In the code above I tried to make the pattern clear.
replaceB()
events happen once in a while, when the app decides it's good time to upload the list of counters to the cloud. This logic is by the nature of it not precise, and it does not matter if the counters are reported in the current or in the next batch. It's important to have sum total of each counter correct, after they are reconsolidated on the server.My solution was to replace
b
with a fresh instance, and slowly process the old instance. My mistake (now, thanks to you comments, I see it) was that I thought I would be safe to allow some counters to get updated on the instance that's being uploaded. I was wrong. There is no way to guarantee that all outstanding notifications (that have captured the old instance) will be processed in time forb.upload()
to actually serialize their results. So, the question whether I needlock
, becomes irrelevant.I will now switch to a model where A keeps two (constant) instances of B, and a (locked) index that chooses the active one, while the other is being uploaded.