Need clarification on dispatch_group_wait() behavior when dispatch_group_create() and dispatch_group_enter() are called from different queues
Asked Answered
A

1

0

I am looking at the Ray Wenderlich tutorial on using dispatch queues to get notified when a group of tasks complete. http://www.raywenderlich.com/63338/grand-central-dispatch-in-depth-part-2

The first code shown under "Code that works" is straight from the tutorial. The Alert view(final completion block) get executed after all 3 downloads complete.

I tried to play around with it and moved the dispatch async down in the "Code that does not work" to see what will happen if dispatch_group_create() and dispatch_group_enter() happen on different queues. In this case, the dispatch_group_enter() does not seem to register because the dispatch_group_wait() immediately completes and alert view(final completion block) is executed even before all the downloads have completed.

Can someone explain whats happening in the second case? (This is just for my understanding of how dispatch group works and I realize thats its better to put the entire function in the global concurrent queue to avoid blocking the main thread).

Code that works

 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
{

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{

    __block NSError *error;
    dispatch_group_t downloadGroup = dispatch_group_create();

    for (NSInteger i = 0; i < 3; i++)
    {
        NSURL *url;
        switch (i) {
            case 0:
                url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
                break;
            case 1:
                url = [NSURL URLWithString:kSuccessKidURLString];
                break;
            case 2:
                url = [NSURL URLWithString:kLotsOfFacesURLString];
                break;
            default:
                break;
        }


            dispatch_group_enter(downloadGroup);
            __block Photo *photo = [[Photo alloc] initwithURL:url
                                  withCompletionBlock:^(UIImage *image, NSError *_error) {
                                      if (_error) {
                                          error = _error;
                                      }
                                      NSLog(@"Finished completion block for photo alloc for URL %@ and photo is %@",url,photo) ;
                                      dispatch_group_leave(downloadGroup);
                                  }];

            [[PhotoManager sharedManager] addPhoto:photo];
            NSLog(@"Finished adding photo to shared manager for URL %@ and photo is %@",url,photo) ;
    }

    dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
    dispatch_async(dispatch_get_main_queue(), ^{
        if (completionBlock) {
            NSLog(@"Executing completion block after download group complete") ;
            completionBlock(error);
        }
    }) ;
  }) ;
}

EDITED Code that does not work with extra NSLog statements

Code that does not work

 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock
 {

__block NSError *error;
dispatch_group_t downloadGroup = dispatch_group_create();

for (NSInteger i = 0; i < 3; i++)
{
    NSURL *url;
    switch (i) {
        case 0:
            url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString];
            break;
        case 1:
            url = [NSURL URLWithString:kSuccessKidURLString];
            break;
        case 2:
            url = [NSURL URLWithString:kLotsOfFacesURLString];
            break;
        default:
            break;
    }

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^{
        dispatch_group_enter(downloadGroup);
        NSLog(@"Enetered group for URL %@",url) ;
        __block Photo *photo = [[Photo alloc] initwithURL:url
                                      withCompletionBlock:^(UIImage *image, NSError *_error) {
                                          if (_error) {
                                              error = _error;
                                          }
                                          NSLog(@"Finished completion block for photo alloc for URL %@ and photo is %@",url,photo) ;
                                          dispatch_group_leave(downloadGroup);
                                      }];

        [[PhotoManager sharedManager] addPhoto:photo];
        NSLog(@"Finished adding photo to shared manager for URL %@ and photo is %@",url,photo) ;
    }) ;
}

NSLog(@"Executing wait statement") ;
dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5
dispatch_async(dispatch_get_main_queue(), ^{
    if (completionBlock) {
        NSLog(@"Executing completion block after download group complete") ;
        completionBlock(error);
    }
}) ;
}
Ape answered 22/2, 2016 at 3:33 Comment(0)
B
3

The "dispatch_group_enter() does not seem to register" because it hasn't actually been called yet by the time that dispatch_group_wait() is called. Or, rather, it's not guaranteed to have been called. There's a race condition.

This isn't specifically about different queues. It's about concurrency and asynchronicity.

dispatch_async() just means "add a task to a list" with an implicit understanding that something, somewhere, somewhen will take tasks off of that list and execute them. It returns to its caller immediately after the task has been put on the list. It does not wait for the task to start running, let alone complete running.

So, your for loop runs very quickly and by the time it exits, it may be that none of the tasks that it has queued have started. Or, if any have started, it may be that they haven't finished entering the group.

Your code may complete its call to dispatch_group_wait() before anything has entered the group.

Usually, you want to be sure that all relevant calls to dispatch_group_enter() have completed before the call to dispatch_group_wait() is made. The easiest way to do that is to have them all happen synchronously in one execution context. That is, don't put calls to dispatch_group_enter() inside blocks that are dispatched asynchronously.

Bergamo answered 22/2, 2016 at 5:5 Comment(5)
Thanks, that makes sense. Pls see edit above. I am seeing that the NSLog statement after "group_ enter" is executing before the NSLog statement before the "dispatch_wait". This seems to point to some root cause other than your explanation above. Should I debug using breakpoints? NSLog outputs in Xcode are 2016-02-21 21:37:01.155 GooglyPuff[393:136916] This is your outer voice: Did I print before or after inner voice? 2016-02-21 21:37:05.319 GooglyPuff[393:136937] Enetered group for URL i.imgur.com/UvqEgCv.png 2016-02-21 21:37:05.319 GooglyPuff[393:136916] Executing wait statementApe
Just executed with breakpoints. First executed 1st dispatch_group_enter and then dispatch_group_wait and then remaining two dispatch_group_enter. Is there a delay between execution of dispatch_group_enter and when the state change gets reflected during a dispatch_group_wait call? Adding the smallest delay ([NSThread sleepForTimeInterval:0.001];) removes the race condition and things are happening in order expected.Ape
Put some output from a run with no breakpoints in your question. I'm not following what you put in the comment above. Adding delays may reduce the window for the race condition, but doesn't necessarily eliminate it. A race condition is a matter of the program logic, not just the vagaries of execution (like whether the system is busy or the scheduler hiccups or whatever).Bergamo
What I meant was: When I ran the code with 2 breakpoints: one at the dispatch_group_enter() line and one at the dispatch_group_wait() line, I see that the dispatch_group_enter() in the first iteration of the for loop is reached first. Since the dispatch_group_enter() has been executed first, why is the dispatch_group_wait() not resulting in a wait? is there some delay between execution of the group_enter() and its state change being reflected? Also, where did you put the output mentioned above "Put some output from a run with no breakpoints in your question." I don't see it in my questionApe
I was asking you to put the output in an edit of the question. It's hard to read in a comment. Also, there are two problems with breakpoints: 1) they, by their nature, change the timing of how the code executes; and 2) if you break at the line with dispatch_group_enter(), you're stopping before it is called, not after. Of course, it takes some amount of time (even if it's very small) to actually execute.Bergamo

© 2022 - 2024 — McMap. All rights reserved.