Maybe I am not able to understand properly, but from what I am seeing, there are few things I would avoid.
I maybe wrong, but I think the problem is that you are launching an async operation on a background queue, but you end up working with the default context queue which you guarantee to be thread safe but it is not. So when you are saving you send notification, which is triggering a save and so on, until you fill up memory. To verify this, try to put a break point in updateMainContext, and see how many times it is invoked.
If you are working with different contexts you should initialize them with an NSPrivateQueueConcurrencyType, this way you are sure that all the work is done in a separate thread. So I would revisit like this in place of dispatch async, in this case you can do this in place of dispatch_async:
NSManagedObjectContext *privateCtx = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[privateCtx performBlock:^() {
// put the code you have in dispatch async...
}];
Then on the other class you should find a way to listen for the private context only, of course you need the reference. If you can't I suppose it is fine to avoid anything that come from the main thread, if that class is only meant to be a point of merge from the background.