Skip to content

Instantly share code, notes, and snippets.

@alexcohn
Last active February 17, 2021 07:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexcohn/b5e0a5f8bef6a55a37372fd8842a85d6 to your computer and use it in GitHub Desktop.
Save alexcohn/b5e0a5f8bef6a55a37372fd8842a85d6 to your computer and use it in GitHub Desktop.
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)
}
}
@alexcohn
Copy link
Author

The question is, lock.synchronized() at line 84 seems irrelevant. But then the whole purpose of lock seems irrelevant, too.

@robertmryan
Copy link

Why is it irrelevant? If you might have another thread updating cnt, then of course it is needed.

@robertmryan
Copy link

FWIW, I really don't get why you'd ever want to both capture b in the observer, but also supply a replaceB, which the observer can never use. Can you give me a practical example where b is a var but you wouldn't want the observer to ever be able to use that new b (and why you'd want to keep both B instances in memory)?

Hey, if b was (a) a reference type (which it is here); (b) it was a constant (e.g. private let b = B() rather than private var b = B(); then, that is a case where capturing b rather than using weak self pattern makes sense. (It's a tad brittle, but it makes sense.) But I can't imagine the above pattern, where you (a) can replace b but (b) can only observe the first instance and can't have the observer act on the current instance; and (c) one keeps the first instance in memory all though you've now replaced it.

@alexcohn
Copy link
Author

Why is it irrelevant? If you might have another thread updating cnt, then of course it is needed.

The whole point is, the cnt is only updated from one thread. Now multiply this by the factor of different UI events that have their separate counters, all updated on the same main thread. lock becomes a significant hassle.

@alexcohn
Copy link
Author

Can you give me a practical example where b is a var but you wouldn't want the observer to ever be able to use that new b (and why you'd want to keep both B instances in memory)?

@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 for b.upload() to actually serialize their results. So, the question whether I need lock, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment