The two methods you've described cover (conceptually) both aspects, however I think you haven't explained sufficiently their pros and cons.
There is one item that you should be aware of, it's the population factor.
- Push method is great when there are many notifiers and few observers
- Pull method is great when there are few notifiers and many observers
If you have many notifiers and your observer is supposed to iterate over every of them to discover the 2 or 3 that are dirty
... it won't work. On the other hand, if you have many observers and at each update you need to notify all of them, then you're probably doomed because simply iterating through all of them is going to kill your performance.
There is one possibility that you have not talked about however: combining the two approaches, with another level of indirection.
- Push every change to a
GlobalObserver
- Have each observer check for the
GlobalObserver
when required
It's not that easy though, because each observer need to remember when was the last time it checked, to be notified only on the changes it has not observed yet. The usual trick is to use epochs.
Epoch 0 Epoch 1 Epoch 2
event1 event2 ...
... ...
Each observer remembers the next epoch it needs to read (when an observer subscribes it is given the current epoch in return), and reads from this epoch up to the current one to know of all the events. Generally the current epoch cannot be accessed by a notifier, you can for example decide to switch epoch each time a read request arrives (if the current epoch is not empty).
The difficulty here is to know when to discard epochs (when they are no longer needed). This requires reference counting of some sort. Remember that the GlobalObserver
is the one returning the current epochs to objects. So we introduce a counter for each epoch, which simply counts how many observers have not observed this epoch (and the subsequent ones) yet.
- On subscribing, we return the epoch number and increment the counter of this epoch
- On polling, we decrement the counter of the epoch polled and return the current epoch number and increment its counter
- On unsubscribing, we decrement the counter of the epoch --> make sure that the destructor unsubscribes!
It's also possible to combine this with a timeout, registering the last time we modified the epoch (ie creation of the next) and deciding that after a certain amount of time we can discard it (in which case we reclaim the counter and add it to the next epoch).
Note that the scheme scales to multithread, since one epoch is accessible for writing (push operation on a stack) and the others are read-only (except for an atomic counter). It's possible to use lock-free operations to push on a stack at the condition that no memory need be allocated. It's perfectly sane to decide to switch epoch when the stack is complete.