39

I have multiple operations (they're AFNetworking requests) with completion blocks that takes some time to execute, and a Core Data object that needs to be saved at the end of all the requests.

MyCoreDataObject *coreDataObject;

AFHTTPRequestOperation *operation1 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation1 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute1 = responseObject;
    sleep(5);
}];
[operation1 start];

AFHTTPRequestOperation *operation2 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation2 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute2 = responseObject;
    sleep(10);
}];
[operation1 operation2];

[context save:nil];

Of course, this does not work as I want because the requests are asynchronous. I tried adding an NSOperationQueue like so:

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue setMaxConcurrentOperationCount:2];

AFHTTPRequestOperation *operation1 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation1 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute1 = responseObject;
    sleep(5);
}];
[operationQueue addOperation:operation1];

AFHTTPRequestOperation *operation2 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation2 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute2 = responseObject;
    sleep(10);
}];
[operationQueue addOperation:operation2];

[imageQueue waitUntilAllOperationsAreFinished];
[context save:nil];

This looks a bit better. Using waitUntilAllOperationsAreFinished, my queue blocks the current thread until my requests are finished, but not until my success Blocks are finished, which is really what I need.

Any ideas on how to achieve this in a good way?

jscs
  • 63,694
  • 13
  • 151
  • 195
choise
  • 24,636
  • 19
  • 75
  • 131

4 Answers4

84

Use dispatch groups.

dispatch_group_t group = dispatch_group_create();

MyCoreDataObject *coreDataObject;

dispatch_group_enter(group);
AFHTTPRequestOperation *operation1 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation1 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute1 = responseObject;
    sleep(5);
    dispatch_group_leave(group);
}];
[operation1 start];

dispatch_group_enter(group);
AFHTTPRequestOperation *operation2 = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
[operation2 setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    coreDataObject.attribute2 = responseObject;
    sleep(10);
    dispatch_group_leave(group);
}];
[operation2 start];

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);

[context save:nil];
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • 1
    thank you, this works well. i am also interested if/how this could be achieved without blocking the thread. any ideas for this? – choise May 18 '12 at 17:25
  • 22
    Um, what could you mean by "**wait** until multiple operations executed" if not blocking the thread? Anyway, if you just want to run a block after all of the completion blocks have run, you can use `dispatch_group_notify(group, queue, ^{ ... });` instead of `dispatch_group_wait()`. – Ken Thomases May 19 '12 at 06:14
  • Alternatively, you can forego completion blocks on the individual operations, and simply iterate through them in the batch completion block, doing what you need all at once. This way wouldn't require any dispatch finagling. – mattt May 22 '12 at 03:47
  • forego? not exactly sure what you mean. do you have an example – choise May 24 '12 at 12:54
  • What doesn't make sense for me about this answer is the question of threads. Presumably this code isn't intended for the main thread (since you should never block that) in which case it is running on a secondary thread. In which case the call to dispatch_group_enter is made on that secondary thread, but dispatch_group_leave is called by AFNetworking on the main thread. is the dispatch_group_t object threadsafe like this? I get crashes on the call to dispatch_group_leave in similar code.. ? – Brynjar Sep 29 '14 at 17:52
  • Yes, the dispatch group API is thread-safe. It would make no sense if it weren't. For example, what could `dispatch_group_wait()` be waiting for, since that blocks the thread and, if the API weren't thread safe, nothing else could call `dispatch_group_leave()`. I suggest you open a new question for your crashes, providing code and the full crash report. – Ken Thomases Sep 29 '14 at 18:09
  • Imagine you would like to add all the `responseObjects` to a NSMutableArray for further processing once all the requests have finished. Would you need to add the calls to`[array addObject:]` in a `@synchronized` block or that won't be necessary? – jjramos Mar 22 '15 at 01:17
  • 1
    You have to use some synchronization mechanism when multiple threads are simultaneously accessing a mutable collection and one or more of them might be mutating it. `@synchronized` is one such mechanism. A serial queue is another. Barrier tasks on a private concurrent queue is another. Etc. – Ken Thomases Mar 22 '15 at 02:04
  • Does the second block wait for the first block to leave the group before entering the group itself? – NYC Tech Engineer Sep 11 '15 at 15:17
  • @NYCTechEngineer, no. The blocks are completion blocks. They aren't intended to run immediately. They run when their respective network operations complete. Which completes first is unpredictable. In any case, there's nothing about the dispatch group that prevents them from running concurrently. That's up to how `AFHTTPRequestOperation` works under the hood. In any case, the blocks don't enter the group. The group is entered (its count is incremented) before the blocks are even created, let alone set as the completion handler, let alone invoked. The blocks leave the group (decrement) at end. – Ken Thomases Sep 12 '15 at 00:16
  • @KenThomases That's what I suspected. I ended up using BFTask, even though I really wanted to use a native solution to implement some kind of promise mechanism. Thanks for the heads up! – NYC Tech Engineer Sep 13 '15 at 01:39
15

AFNetworking has designed method for this kind of operations, which abstracts from GCD:

-enqueueBatchOfHTTPRequestOperationsWithRequests:progressBlock:completionBlock:

-enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock:

Take a look at FAQ

wzs
  • 1,057
  • 9
  • 12
1

I belive something like this:

NSMutableArray *mutableOperations = [NSMutableArray array];
for (NSURL *fileURL in filesToUpload) {
    NSURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileURL:fileURL name:@"images[]" error:nil];
    }];

    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    [mutableOperations addObject:operation];
}

NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:@[...] progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
    NSLog(@"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
    NSLog(@"All operations in batch complete");
}];
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];

refering to docs: http://cocoadocs.org/docsets/AFNetworking/2.5.0/

yershuachu
  • 778
  • 8
  • 19
1

My requirements were those of do many request from an array of String (URL)

func updateSourceData(element: Int) {

    if element > availableUrls.count - 1 {
        return
    }

    let service = SourceDataServiceDao()
    let currentUrl = availableUrls[element]
    service.fooCall(url: currentUrl, completion: { (response, error) -> Void in
        self.updateSourceData(element + 1)
    })

}

Obviously, in this way calls are made in cascade, not N asynchronous calls.

Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146