Next article: Friday Q&A 2010-01-15: Stack and Heap Objects in Objective-C
Previous article: Friday Q&A 2010-01-01: NSRunLoop Internals
Tags: cocoa fridayqna notifications
It's that time of the week again. No, it's not just time to go get drunk, but time for Friday Q&A! This week's topic, suggested by Christopher Lloyd of Cocotron (a really neat open source project that lets you write Objective-C/Cocoa code for non-Mac platforms like Windows), is NSNotificationQueue
, a little-known, poorly-understood, but handy Foundation class.
Runloops
NSNotificationQueue
works in close concert with NSRunLoop
. If you haven't already, it might be a good idea to read my post from last week on NSRunLoop internals.
(No) Threading
Many people, upon confronted with NSNotificationQueue
, immediately think that it allows posting notifications across threads. (Well, I did, anyway.) In fact, NSNotificationQueue
is completely unrelated to threads! Like NSRunLoop
, there exists a single NSNotificationQueue
per thread, and that per-thread instance can't be used from other threasd.
So What Is It?
Simply put, the primary mission of NSNotificationQueue
is to delay posting of a notification, and to allow coalescing of notifications.
Delayed Notifications
Sometimes you don't want to post a notification immediately. This is especially true if you want to coalesce multiple identical notifications; you can't do that unless you delay posting the first one. It can be useful for other scenarios as well, such as wanting to give calling code a chance to run before the notification's observers run. (In this respect, it's very similar to passing zero delay to performSelector:withObject:afterDelay:
in order to have some code run at the next runloop cycle.)
NSNotificationQueue
provides three posting styles for notifications:
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
NSPostNow
is the easiest to understand. This has the same semantics as posting the notification directly to the NSNotificationCenter
, in that the notification is posted immediately and observers are notified before control returns to the caller. The only reason to use this instead of NSNotificationCenter
is because it can be used to coalesce previously enqueued notifications before posting.
NSPostASAP
is much like a zero-delay timer. The notification is not posted immediately, but will be posted as soon as control returns to the runloop. The usage scenarios are much like for a zero-delay timer.
NSPostWhenIdle
will wait until the runloop is idle, then post the notification. You can think of this as using a zero-delay timer with low priority. As long as the runloop has other work to do, the notification will not be posted. Once the runloop runs out of stuff to do, the notification will be posted. This is useful if you want to wait until your program has not only finished the currently executing code, but has nothing else to do. For example, imagine tracking mouse movement and performing an expensive update on the basis of that movement. Performing that update with every movement will make the program unresponsive, but you still want to update as often as possible within reason. Using NSPostWhenIdle
will accomplish this. As long as more mouse movement events are pending, the notification will not be posted. If the user pauses for a moment, the runloop will clear out and the notification will be posted. If the user has a really fast computer, or the computation takes less time than expected, the runloop may have time to idle in between mouse moved events, and your update will then happen more frequently.
Coalescing
The real power of NSNotificationQueue
is in coalescing. What does that mean?
When posting with NSPostASAP
or NSPostWhenIdle
, the notification is not posted immediately, but rather is queued. Coalescing means that if a notification is posted which matches one already in the queue, the two are merged, so that only a single notification is posted to observers.
This behavior can be really handy. Imagine a loop modifying a bunch of objects which posts notifications that cause other parts of the application to update their idea of the world. If they only need one notification at the end of all the modifications, coalescing will allow that other code to avoid a lot of needless computation that would occur if every modification caused a separate notification.
NSNotificationQueue
provides three types of coalescing.
NSNotificationNoCoalescing
means that no coalescing is performed.NSNotificationCoalescingOnName
means that coalescing is performed if two notifications share the same name.NSNotificationCoalescingOnSender
means that coalescing is performed if two notifications share the same sender object.
NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender
means that coalescing is performed if two notifications share both the same name and the same sender object. (Most of the time when coalescing, this is what you want, and it's what -enqueueNotification:postingStyle:
implicitly uses.)
With coalescing, the semantics of NSPostNow
make more sense. By using NSPostNow
with NSNotificationQueue
, any matching enqueued notifications will be coalesced with the one being posted before it's posted, essentially clearing them out and posting the coalesced notification earlier than would have otherwise happened.
These coalescing flags can also be used to remove notifications from the queue without posting them, using the -dequeueNotificationsMatching:coalesceMask:
method.
Examples
NSNotificationQueue
is pretty straightforward to use. Rather than standard code using NSNotificationCenter
, you more or less drop in NSNotificationQueue. As a concrete example, imagine you have an NSSlider
set to be "continuous", so that it sends its action message every time the mouse is moved, even while it's down:
- (IBAction)sliderMoved: (id)sender
{
[self updateWithNewSliderValue: [sender doubleValue]];
NSEventTrackingRunLoopMode
while the mouse is down, you can just post a notification using NSPostASAP
onto the NSNotificationQueue
, and the notification will be posted only once the mouse is released. By coalescing, this code ensures that only one notification is posted when the mouse is released, even though this action message may be called many times:
NSNotification *note = [NSNotification notificationWithName: SliderDoneMovingNotification object: self];
[[NSNotificationQueue defaultQueue] enqueueNotification: note postingStyle: NSPostASAP];
NSPostWhenIdle
and NSEventTrackingRunLoopMode
will allow this:
note = [NSNotification notificationWithName: ExpensiveSliderUpdate object: self];
NSArray *modes = [NSArray arrayWithObject: NSEventTrackingRunLoopMode];
[[NSNotificationQueue defaultQueue] enqueueNotification: note
postingStyle: NSPostWhenIdle
coalesceMask: NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender
forModes: modes];
}
SliderDoneMovingNotification
and remove ExpensiveSliderUpdate
from the queue:
- (void)sliderDoneMoving: (NSNotification *)note
{
NSNotification *note = [NSNotification notificationWithName: ExpensiveSliderUpdate object: self];
[[NSNotificationQueue defaultQueue] dequeueNotificationsMatching: note
coalesceMask: NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender];
}
NSNotificationQueue
may not be well known in general, but now you know how it works, what it's good for, and have some ideas for how to put it to work.
That's it for this week. Come back next week for another exciting edition. (A warning: I'm going to be making a long trip not long before next Friday, and so there's some possibility that I'll miss next week's edition. In that event, my instructions to you, the reader, are to panic as thoroughly as possible until the following Friday.)
As always, Friday Q&A is driven by reader suggestions. If you have an idea for a topic to cover here, please send it in!
Comments:
If you can find any more weird, obscure, useful classes to expound upon, let me know!
Would these be declared in the controller that is the target of the slider?
I'm assuming so, but I'm a little rusty on my Cocoa and not in front of a Mac to test (for shame!).
Also, this solidifies the run loop concepts even more, so bonus!
For any thread that has a finite life, be careful when using anything but NSPostNow. The delayed notifications are not guaranteed to be sent. I don't know of an elegant solution; one workaround is to give the current runloop an extra workout before the thread dies. In practice I've seen delayed notifications on a background thread lead to many bugs when not managed carefully.
It depends on what you're trying to acheive but NSOperation dependencies offer an alternate for "do this afterwards" when you're off the event loop (but need to be configured before the initial work begins).
sliderMoved:
would be set as the target of the slider. You'd also need some code somewhere (in the init method, probably) to add the object as an observer for SliderDoneMovingNotification
with sliderDoneMoving:
as the observation method. There's no automatic hookup for that sort of thing, unfortunately.
Sarah Holbrook: The problem of secondary threads with finite lifetimes is an interesting one. I can see a few reasonable solutions:
1) Only use
NSNotificationQueue
for things that can withstand not being delivered.
2) Ship the queued notifications to the main thread, or to another thread which lives forever.
3) When exiting the thread, add another notification with
NSPostWhenIdle
, then spin the runloop until it fires. This should mean all other pending notifications have fired as well... assuming none of the observers use NSNotificationQueue
!
Definitely takes some added thought and work for this case, though.
Speaking of
NSOperation
, using NSNotificationQueue
from an NSOperationQueue
or a GCD queue could be a really good way to run into trouble.Adding one more notification (#3) seems like it could work. I'm curious if anyone's tested it. If there were another "idle" notification is it guaranteed to send first? I haven't verified if the queue is mostly in name or whether it's truly FIFO with delayed notifications.
-performSelectorOnMainThread:
to be troublesome when used in a tight loop. It operates at a high priority and can cause the app to become unresponsive if you aren't careful.
For #3, from the docs:
A notification queue maintains notifications (instances of NSNotification) generally in a first in first out (FIFO) order.
That word "generally" is a bit disconcerting, but I think that refers to the fact that e.g. NSPostASAP notifications will happen before NSPostWhenIdle even if the NSPostASAP happened later.
I have not tested it, though, so it could be way wrong.
I sort of wish they hadn't split NSNotification and NSNotificationQueue but instead provided a single interface and allow flags or calling semantics.
I'm sure I'll use soon!
So I break my code in small portions. At the end of a portion I sent a notification with NSPostWhenIdle. When the notification was post I proceed with the next portion. It worked very well.
NSNotificationCenter
to add coalescing/delayed methods that just call through to NSNotificationQueue
, if you wanted to. Of course, if your grip is that NSNotificationQueue
is hard to discover, that doesn't help at all.NSNotificationQueue
. See the "Using NSNotificationQueue Warning" section in the Foundation release notes <URL:http://developer.apple.com/mac/library/releasenotes/Cocoa/Foundation.html&;gt;. I've taken the liberty of quoting the passage in its entirety:
NSNotificationQueue is an API and mechanism related to run loops (NSRunLoop), and the running of run loops. In particular, posting of notifications via the notification queue is driven by the running of its associated run loop. However, there is no definition for what run loop a notification queue uses, and there is no way for a client to configure that. In fact, any given notification queue might be "tickled" into posting by nearly any run loop and different ones at different times (and given that enqueued notifications are also mode-specific, what mode(s) the run loop(s) are running in any any given time also come into play). Further, although each notification queue is "thread-specific" (see the +defaultQueue documentation), there has never been any relationship between that thread and the run loop used by a notification queue. This is all more and more problematic as more and more things move to happening on different and new threads.
The practical upshot is:
- Notifications posted through a queue might not be posted in any particular "timely" fashion;
- Notifications posted through a queue might not ever be posted (the run loop of the thread that the queue chose to ask to poke it might not be run again; for example, the thread of that run loop might exit and the notification queue discarded);
- Notifications can be posted by any given queue on a different thread than the thread the queue is the default queue for (when a queue is a default queue for some thread);
- Notifications can be posted by any given queue on different threads over time;
- There is no necessary/guaranteed relationship between the thread(s) on which notifications are enqueued and those on which the notifications eventually get posted (delivered) for any notification queue
This is true for all releases of Mac OS X. Foundation and AppKit do not use NSNotificationQueue themselves, partly for these reasons.
Far too much useful information is hidden away in release notes and never makes it into the permanent documentation. At the very least, they could have updated the
NSNotificationQueue
class reference documentation to include the warning.
The practical upshot of this is that the release notes are essential reading if you want to learn the fine and ugly details of the frameworks as they stand, rather than as they were documented four or five years ago. This, unfortunately, significantly reduces the utility of the documentation that you can readily access from Xcode: essential information is no longer just an Option-double-click away.
Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.
It's definitely one of those classes that you look at in the docs and think "that's useful, wonder what I should use it for", and then you promptly forget about it.
More topics like this please! (And the NSRunloop post was great too). I'll scan through Foundation.framework and look for other obscure but useful classes.