GCD Dispatch Groups With an Additional Level of Inception

We often find ourselves in a situation where we have to wait for multiple asynchronous blocks to finish before we can proceed. One such case is even described in Apple’s Concurrency Programming Guide. It can be easily done using dispatch groups functions. But things may get complicated when the code inside each of your blocks makes other asynchronous calls. And it’s the completion of the latter ones that you want to be notified of.

What’s the Problem?

Suppose we have some time-consuming task that we need to perform multiple times asynchronously. It can be some delegate method with a callback, an AFNetworking request operation with success and failure blocks or just a NSURLConnection completion handler. Most likely something beyond our control. The important thing is that somewhere down the line it makes another *_async call. In our example, that task will look like this:

- (void)taskWithNumber:(NSUInteger)number andCompletionBlock:(void (^)(void))completionBlock
{
  NSLog(@"Task number %d", number);
  usleep(arc4random_uniform(SLEEP_TIME));

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    usleep(arc4random_uniform(SLEEP_TIME));
    completionBlock();
  });
}

First Approach and Failure

If we will follow Apple’s example mentioned above (Waiting on Groups of Queued Tasks) with dispatch_group_async and dispatch_group_wait we will probably end up with something like this:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSUInteger i = 0; i < TASKS_COUNT; ++i) {
  dispatch_group_async(group, queue, ^{
    [weakSelf taskWithNumber:i andCompletionBlock:^{
      NSLog(@"End of task %d", i);
    }];
  });
}

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"All tasks done");

Unfortunately, this doesn't solve our problem - we want to wait until all completion blocks of the background task are finished. In the code above, dispatch_group_wait will block execution only until all tasks are done but not their async parts. If you look at the output, you will find the "All tasks done" log message but certainly not at the end:

GCDGroupsDemo[261:7e03] End of task 191
GCDGroupsDemo[261:3603] End of task 176
GCDGroupsDemo[261:c303] End of task 207
GCDGroupsDemo[261:70b] All tasks done
GCDGroupsDemo[261:cd03] End of task 127
GCDGroupsDemo[261:9103] End of task 108
GCDGroupsDemo[261:d003] End of task 208

Not That Pretty But It’s Working

So what else can we do? We can introduce a block variable that will initially hold a number of tasks and decrease it in each completion block. Then we can check whether the counter reached 0, which would mean that we can execute our final code. But the counter alone is not enough. If we are running our code in a concurrent queue (like one of the global queues), we will most likely run into a series of race conditions. This will cause our counter to decrease incorrectly and the final code will never get executed. To make sure only one thread at time updates the counter, we have to synchronize access to it. Additionally, when using local variables we have to ensure that no other threads change inProgress between the @synchronized block and the if where we check its value. Tasks can be dispatched using any asynchronous GCD function. Below, I only used dispatch_async.

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
__block NSUInteger inProgress = TASKS_COUNT;

for (NSUInteger i = 0; i < TASKS_COUNT; ++i) {
  dispatch_async(queue, ^{
    [weakSelf taskWithNumber:i andCompletionBlock:^{
      NSLog(@"End of task %d", i);
      NSUInteger = localInProgress;

      @synchronized(weakSelf) {
        localInProgress = --inProgress;
      }

      if (localInProgress == 0) {
        NSLog(@"All tasks done");
      }
    }];
  });
}

It works as expected and here's the last part of output:

GCDGroupsDemo[641:5603] End of task 9963
GCDGroupsDemo[641:4e03] End of task 9993
GCDGroupsDemo[641:7c03] End of task 9962
GCDGroupsDemo[641:7c03] All tasks done

Going Deeper to Make It Better

So we have a working solution, but it's really not that elegant. The inProgress counter is not self-explanatory, it has to be synchronized and the final code is nested inside each completion block. To improve it, we have to go back to dispatch groups. If you take a look at the GCD Reference you will eventually find that there are two additional functions related to dispatch groups. These are dispatch_group_enter and dispatch_group_leave both of them absent from the Concurrency Programming Guide. After reading their documentation (especially the Discussion section) you can see that we can replace the inProgress counter with paired calls to them.

All that these functions do is increase and decrease the group's internal tasks reference counter so we can manually decide when to enter or leave the group. Actually, if you dig into the GCD source code you will find that dispatch_group_async also uses them to manage the tasks reference count. So it's a slightly lower layer but still within the public API of GCD.

Now, we can also move our final code out of the completion block, like in the first example. It will be a better idea to use dispatch_group_notify than dispatch_group_wait this time because it won't block the user interface. The complete solution is cleaner and works exactly like the previous one:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSUInteger i = 0; i < TASKS_COUNT; ++i) {
  dispatch_group_enter(group);
  dispatch_async(queue, ^{
    [weakSelf taskWithNumber:i andCompletionBlock:^{
      NSLog(@"End of task %d", i);
      dispatch_group_leave(group);
    }];
  });
}

dispatch_group_notify(group, queue, ^{
  NSLog(@"All tasks done");
});

Summary

dispatch_group_enter and dispatch_group_leave are both quite powerful; for example, you can use them to assign a block to more than one dispatch group at the same time. However, due to their simple nature, they are also quite error-prone - the programmer is responsible for balancing calls to them and it can be quite tricky when things get complicated. Obviously, these functions should not be overused and in most cases dispatch_group_async is all you need. But it's good to remember they're around because sometimes we need more control over our asynchronous code.

Working with Collections Using Blocks – Introducing MCSCollectionUtility Repository
Calling Blocks Inline