BluGeni
BluGeni

Reputation: 3454

block within a block not sure what to do

I am making a mess and am trying to figure out a proficient way to handle this but am getting stuck. My problem right now is that I have code executing before I want them to be and it has to do with my blocks.

The problem with the code below is that the postToForm method is being called and executed before my convertedImages have been added to. How can I fix this so it waits for that to happen? Or am I going about this the whole wrong way?? (I need to be aware of memory usage too, need to release memory after a post has been made.)

Here is my code that contains the problem:

// create serial queue
dispatch_queue_t queue = dispatch_queue_create("myQueue", 0);

// create dispatch group
dispatch_group_t group = dispatch_group_create();

for(id photos in photoUrls) {
    NSString *urlString = photos;

    // enqueue operation in queue
    dispatch_async(queue, ^{
        // enter the group
        dispatch_group_enter(group);

        // do something async, I do use another dispatch_queue for example
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
            // wrap in autoreleasepool to release memory upon completion
            @autoreleasepool {
                // here for example the nested operation sleeps for two seconds
                NSURL *url = [NSURL URLWithString:urlString];
                ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

                [library assetForURL:url resultBlock:^(ALAsset *asset) {

                    //TODO: Deal with JPG or PNG
                    NSData *imageData = UIImageJPEGRepresentation([self thumbnailForAsset:asset maxPixelSize:1000], 0);
                    NSString *base64 = [imageData base64EncodedString];
                    NSLog(@"base64::%@",base64);
                    [convertedImages addObject:base64];

                } failureBlock:^(NSError *error) {
                    NSLog(@"that didn't work %@", error);
                }];


                dispatch_group_leave(group);
            }
        });

        // wait until the nested async operation leaves the group (e.g finishes its job)
        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

        NSLog(@"Finished single operation.");
    });
}

// will be called once all operations complete
dispatch_async(queue, ^{
    NSLog(@"Finished all jobs. ");
    NSLog(@"how many::%lu",(unsigned long)convertedImages.count);
    [jsonWithPhotos setObject:convertedImages forKey:@"photo64"];
    [self postToForm];
});

Upvotes: 2

Views: 180

Answers (4)

Antonio E.
Antonio E.

Reputation: 4391

The reason why your completion block is called before that all images are loaded is that you are calling dispatch_group_leave(group); in the wrong place. You should call it in the result block (and error block) of the ALAssetsLibrary. The rest is correct (in fact is exactly as Apple suggest to implement a completion block of a queue: since GCD's private queue are serials, a completion block is nothing more that the last block of the queue itself).

EDIT: As asked: The fact is that the ALAssetsLibrary take some time to extract the data at the requested url (in fact there is a completion block, that hints that the operation is asynchronous). So immediately after requesting the image for the current url you were releasing the semaphore created by the dispatch_group_enter and probably this was happening BEFORE the ALAssetsLibrary could execute the completion block.

Now, mind that how you implemented the queue's completion block is valid for serial queue only, from iOS 5+ you can create concurrent queue (as the global queue are) by specifying the DISPATCH_QUEUE_CONCURRENT as queue type and there this kind of approach is not valid anymore. If instead of having a serial queue you had a concurrent one in this case I would have checked if the current url were the last one inside the ALAssetsLibrary result block and called there the completion block.

For completeness, you can use the dispatch_semaphore_t to handle such situations, that is logically more correct than using a group wait (here your group is composed by 1 operation), but, I repeat, is only logically better, the results is the same.

Everything I wrote is written in the Apple's concurrency programming guide

Upvotes: 4

Duncan Groenewald
Duncan Groenewald

Reputation: 8988

Why bother running each task in the loop as a separate async job ? Run the entire loop as a single task and in that completion handler call a function on your main thread to postToForm.

Something like

   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 
                                         (unsigned long)NULL), ^(void) {
      [self convertImages];

   });

   // Call this on background thread
   - (void)convertImages {

      for(id photos in photoUrls) {
         // Do stuff  
      }

      // Now call function on main thread
      [[NSOperationQueue mainQueue] addOperationWithBlock:^ {
              [self updateUI];
      }];
    }

    // Must call this on main thread because we update the UI
    - (void)updateUI {
           NSLog(@"Finished all jobs. ");
           NSLog(@"how many::%lu",(unsigned long)convertedImages.count);
           [jsonWithPhotos setObject:convertedImages forKey:@"photo64"];
           [self postToForm];
    }

Upvotes: 0

Travis
Travis

Reputation: 3369

If you have dependencies, you'll want to use NSOperationQueues. With operation queues you can set dependence between operations such that one (or more) will not execute until a particular operation is finished.

So, for example, using blocks (like the ones you have) you can implement it something like as follows.

NSBlockOperation* operation = [NSBlockOperation blockOperationWithBlock:^{ /* your work */ }];
NSBlockOperation* dependentOperation = [NSBlockOperation blockOperationWithBlock:^{ /* dependent work */ }];

[dependentOperation addDependency:operation];

NSOperationQueue* q = [[NSOperationQueue alloc] init];
[q addOperations:@[ operation, dependentOperation] waitUntilFinished:NO];

NSOperationQueues also buy you a few neat things like being able to handle cancellations, which can come in handy if you're worried about cancelling the postToForm if there are failures elsewhere.

And if you're worried about memory, you can set the maximum number of concurrent operations as a part of your NSOperationQueue.

q.maxConcurrentOperationCount = 5; 

Upvotes: -1

Putz1103
Putz1103

Reputation: 6211

If you want to use dispatch_async for all of this then you are going to have to function a bit differently. Form what I can tell you are just trying to do all of this in the background. There are a few different ways to do this, but I see your code going in a single block in a background thread and then calling back when it's completed. This will also be the most memory efficient (not bloating) because it runs one conversion at a time instead of trying to do them all at once and filling up your available memory (and possibly crashing).

dispatch_async into queue
{

    for loop to do all of your images
    {

    }

    dispatch_async into main thread saying finished queue
    {
        //function call or block with executing code
    }
}

Wrap all of that in an autoreleasepool just for added memory management fun.

Upvotes: 0

Related Questions