Notification Service Extension and the main thread

Question, if I am writing async code in the notification service extension, I understand it terminates after 30 seconds.

If I want to wait until these async methods finish before calling the content handler, I believe an option I have is to use dispatch groups. However I am open to other solutions if there are better options.

My question is, if I use dispatch groups, is there any issue in using the main queue here? Or does the main thread not make sense to use in the context of the NSE?

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    if (self.contentHandler) {
        self.contentHandler(self.bestAttemptContent);
    }
});

Or is it recommended to instead use a different queue in the NSE?

dispatch_queue_t nseQueue = dispatch_queue_create("com.blah.blah.nse.queue", DISPATCH_QUEUE_SERIAL);
dispatch_group_notify(group, dispatch_get_global_queue(QOS_CLASS_(SOMETHING), 0), ^{ ... });

OR am I over thinking this? :) Thanks ahead of time, relatively new to iOS so just looking to learn/understand better.

Question, if I am writing async code in the notification service extension, I understand it terminates after 30 seconds.

Correct, though I always recommend that anyone setting up things like timeouts use a shorter value "just in case". So I'd probably build around ~25s, not 30s.

If I want to wait until these async methods finish before calling the content handler, I believe an option I have is to use dispatch groups. However, I am open to other solutions if there are better options.

What are you actually waiting on? In general, I've become very nervous anytime I see code that uses dispatch groups because they seem to be used as a slightly awkward "band-aid" trying to make something work that doesn't really want to work. Case in point here, the main reason an NSE would be waiting is network activity, in which case the simpler solution would be to simply set the right timeout on that network activity.

Having said that....

My question is, if I use dispatch groups, is there any issue in using the main queue here? Or does the main thread not make sense to use in the context of the NSE?

...the main queue will fine here. GCD requires the main queue to be defined as part of its core architecture, and I believe NSE is actually using a standard run loop (not dispatch_main). Assuming you're not doing something silly (like blocking the main thread for 20+s), I don't see any issue with using the main queue for something like this.

Or is it recommended to instead use a different queue in the NSE? OR am I overthinking this? :) Thanks ahead of time, relatively new to iOS so just looking to learn/understand better.

Well, you said you wanted to learn...

The first thing to understand was that GCD was built to provide a low-level thread pool API that all of the system could use, and that dynamic heavily drove a lot of its core design. For example, it was written as a C API (not ObjC) because it had to be usable by components underneath Foundation, not because anyone at Apple thought C was the best language for managing threads. In most cases, the best option is to use "something else" as there is some other, higher-level, API option that is simply "better".

However, if you're going to build on GCD, my actual recommendation will sound somewhat contradictory. Basically:

  1. Queues are great! Make as many of them as it takes to create a good representation of your app’s internal architecture and data flow.

  2. Queues are TERRIBLE! You probably only need one, and you should NEVER use the global concurrent queues.

You'll find a long discussion of this on this thread, but what's going on here is that GCD queues actually serve two very different roles.

The first role (queues are great!) is about "labeling" work and managing how that work is serialized against itself. In that context, more queues are great because they let you better notate the specific work your app is doing, and more information is almost always better.

The second role (queues are TERRIBLE!) is in how they're used to actually schedule and manage work. In practice, it turns out that the "best" approach for LOTS of situations ends looking a lot like this:

  1. Use the "right" (small) number of threads. (Fastest, not too complicated)

  2. Use one thread (Pretty fast, really simple)

  3. Let GCD run wild with lots of queues (Really slow, really complicated)

...and, unfortunately, it turned out that GCD’s general structure and the way we taught people about it tended to encourage number 3. In the worst case, that basically turned GCD into a great tool for making slower, buggier apps.

In any case, the solution to these issues is DispatchQueue.setTarget(queue:), using the approach I described here. That approach lets you manage how your work is logically represented (#1) while maintaining a simple, serial implementation (avoiding #2).

Also, quoting myself from that post:

*One detail to understand here is that queues aren't objects that work actually "moves through". So a block doesn't "arrive" at your queue, then move to its target queue, then the next target queue, then the next... until it reaches a global queue. Simply having lots of queues or nested target queues doesn't affect performance.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you for that very insightful response! I appreciate it, and learned a lot.

Responding to your question below. Hopefully this will give a bit more context to what I am doing and why.

What are you actually waiting on? In general, I've become very nervous anytime I see code that uses dispatch groups because they seem to be used as a slightly awkward "band-aid" trying to make something work that doesn't really want to work. Case in point here, the main reason an NSE would be waiting is network activity, in which case the simpler solution would be to simply set the right timeout on that network activity.

For example, let's say 3 async things are happening:

  1. Call a reporting API
  2. Call a service metric API
  3. Download an attachment

I don't want to call contentHandler until I have given them all adequate time to complete. My understanding is once contentHandler is called, the system will kill the NSE process.

Alternatively I could not call the contentHandler, and let the full ~25-30 seconds run and let the system kill it. However for obvious reasons I wouldn't want to do that and I doubt that would be an encouraged solution.

I am thinking about doing something that approximates the following. Let me know if this makes sense or if I am over complicating it.

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
[self reportReceivedWithTimeout:5 completion:^(BOOL success) {
    dispatch_group_leave(group);
}];

dispatch_group_enter(group);
[self publishMetricsWithTimeout:5 completion:^(BOOL success) {
    dispatch_group_leave(group);
}];

dispatch_group_enter(group);
[self downloadAttachmentWithTimeout:5 completion:^(BOOL success) {
    dispatch_group_leave(group);
}];

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    [self callContentHandler];
});

That being said, I think your point, as I understand it, is it may be far simpler if I just force the code to be synchronous and use timeouts to ensure it will all execute within the 25-30 seconds.

Let me know if I am taking away the wrong conclusion.

Notification Service Extension and the main thread
 
 
Q