I thought I would document my design decisions here in case they're useful to others. My final solution was based on TechZen's answer.
First, I'll start with a short, and hopefully clearer, restatement of the problem:
In my application, I want to detect changes to a specific attribute (price) of a managed object (Product). Furthermore, I want to know about those changes whether they're made on the main or a background thread. Finally, I want to know about those changes even if the main thread currently does not have the changed Product object in its managed object context.
The NSManagedObjectContextObjectsDidChangeNotification generated by Core Data indicates that a managed object has changed, but doesn't indicate which attribute has changed. My kludgy solution was to create a Price managed object containing a single price attribute, and to replace the price attribute in Product with a to-one relationship to a Price managed object. Now, whenever a change is made to a Price managed object, the Core Data NSManagedObjectContextObjectsDidChangeNotification will contain that Price object in its NSUpdatedObjectsKey set. I simply need to get this information to the main thread. This all sounds good, but there's a hitch.
My Core Data store is being manipulated by two threads. This is done in the "usual" way—there is a managed object context for each thread and a single shared persistent store coordinator. After the background thread makes changes, it saves its context. The main thread detects the context save via the NSManagedObjectContextDidSaveNotification and merges the context changes using mergeChangesFromContextDidSaveNotification:. (Actually, since notifications are received in the same thread they're posted in, the NSManagedObjectContextDidSaveNotification is received on the background thread and passed to the main thread via performSelectorOnMainThread: for merging.) As a result of the merge, Core Data generates a NSManagedObjectContextObjectsDidChangeNotification indicating the changed objects. However, as far as I can tell, the NSManagedObjectContextObjectsDidChangeNotification only includes those objects which are currently represented in the receiving context. This makes sense from the perspective of updating the UI. If a managed object isn't being displayed, it probably won't be in the context, so there's no need to include it in the notification.
In my case, my main thread needs to know about changes made to managed objects whether or not they're currently in the main thread's context. If any price changes, the main thread needs to queue an operation to process that price change. Therefore, the main thread needs to know about all price changes even if those changes are made on a background thread to a product that's not currently being accessed on the main thread. Obviously, since NSManagedObjectContextObjectsDidChangeNotification only contains information about objects currently in the main thread's context, it doesn't meet my needs.
The second option I thought of was to use the NSManagedObjectContextDidSaveNotification generated by the background thread when it saves its context. This notification contains information about all changes to managed objects. I already detect this notification and pass it to the main thread for merging, so why not peek inside and see all of the managed objects that have changed? You'll recall that managed objects are not meant to be shared across threads. Consequently, if I start examining the contents of NSManagedObjectContextDidSaveNotification on the main thread, I get crashes. Hmm ... so how does mergeChangesFromContextDidSaveNotification: do it? Apparently, mergeChangesFromContextDidSaveNotification: is specifically designed to work around the "don't share managed objects across threads" restriction.
The third option I thought of was to register for NSManagedObjectContextDidSaveNotification on the background thread and while still on the background thread convert its contents into a special PriceChangeNotification containing object IDs instead of managed objects. On the main thread, I could convert the object IDs back into managed objects. This approach would still require the to-one Price relationship so that changes in prices are reflected as changes to Price managed objects.
I based my fourth option on TechZen's suggestion to override the price setter in the Product managed object. Rather than use a to-one relationship to force Core Data to generate the notifications I needed, I went back to using a price attribute. In my setPrice method, I post a custom PriceChangeNotification. This notification is received on the background thread and is used to construct a set of Product objects with price changes. After the background thread saves its context, it posts a custom PricesDidChangeNotification which includes the object IDs of all Product objects whose prices have changed. This notification can be safely transferred to the main thread and examined because it uses object IDs instead of managed objects themselves. On the main thread I can fetch the Product objects referenced by those object IDs and queue an operation to perform the lengthy "price change" calculation on a new background thread.