0

I'm trying to make network requests on a background thread and I've decided to use NSBlockOperations. I'm using ADNKit to handle my fetch requests. Here's the code:

- (void)reloadPosts 
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        __block NSArray *additionalPosts;
        __block ANKAPIResponseMeta *additionalMeta;
        __block NSError *additionalError = nil;

        NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
            PRSPostStreamDataController *strongSelf = weakSelf;

            // update data or handle error
            [strongSelf.data setPosts:additionalPosts withMeta:additionalMeta];
        }];

        NSBlockOperation *firstPostsOperation = [NSBlockOperation blockOperationWithBlock:^{
            PRSPostStreamDataController *strongSelf = weakSelf;
            NSDictionary *response = [strongSelf refreshPostsInPart:PartFirst];
            firstPosts = [response objectForKey:@"posts"];
            firstMeta = [response objectForKey:@"meta"];
            firstError = [response objectForKey:@"error"];
        }];

        [completionOperation addDependency:firstPostsOperation];
        [self.queue addOperation:firstPostsOperation];
        [self.queue addOperation:completionOperation];

    });
}


- (NSDictionary *)refreshPostsInPart:(StreamPart)part
{
    // get pagination IDs from data object
    ANKPaginationSettings *pagination = [[ANKPaginationSettings alloc] init];
    pagination.beforeID = [data beforeIDForPart:part];
    pagination.sinceID = [data sinceIDForPart:part];
    pagination.count = 20;

    // authenticatedClient is an object returned from a singleton managing accounts
    ANKClient *client = [authenticatedClient clientWithPagination:pagination];
    __block NSMutableArray *posts = [NSMutableArray new];
    __block ANKAPIResponseMeta *m = nil;

    __block BOOL isMore = YES;
    __block NSError *err;

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block NSString *originalMaxID;
    while ((isMore) && (!err)) {

        self.apiCallMaker(client, ^(id responseObject, ANKAPIResponseMeta *meta, NSError *error){
            if (!error) {
                if (!originalMaxID) {
                    originalMaxID = meta.maxID;
                }
                m = meta;
                [posts addObjectsFromArray:(NSArray *)responseObject];

                client.pagination.beforeID = meta.minID;
                isMore = meta.moreDataAvailable;

            } else {
                err = error;
            }

            // signal that we are ready for the next iteration of the while loop
            dispatch_semaphore_signal(semaphore);
        });

        // wait for the signal from the completion block
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }

    if (!err) {
        m.maxID = originalMaxID;
    }

    NSMutableDictionary *response = [NSMutableDictionary new];
    if (posts) [response setObject:posts forKey:@"posts"];
    if (m) [response setObject:m forKey:@"meta"];
    if (err) [response setObject:err forKey:@"error"];

    return response;
}

...
typedef void (^APIPostListCallback)(id responseObject, ANKAPIResponseMeta *meta, NSError *error);


- (void (^)(ANKClient *client, APIPostListCallback callback))apiCallMaker
{
    return [^(ANKClient *client, APIPostListCallback callback) {
        [client fetchPostsMentioningUser:self.user completion:callback];
    } copy];
}

My code should be self explanatory however when I call self.apiCallMaker I'm referencing a property defined in a configuration object. See this question that I asked earlier for more details on whats happening with that property

I'm having trouble keeping my interface from stuttering when I try and fetch more than 40 posts. The way I've divided my data is into 1 - 5 parts, each part can contain from 1 - 200+ posts. Of course, when I can, I trim these down. My problem is that when I reload all my data, I reload each section with one of these NSBlockOperations. I've only shown one here to keep it as concise as possible. I've tested this out in instruments and each time the ANKClient object goes to convert its JSON response into ANKPost objects, my CPU is pegged at upwards of 100% and my interface is stutters.

Questions:

  1. Is the converting of the JSON response into ANKPost objects executed by the completion handlers of ANKClient done off the main thread?
  2. Is everything executed in each NSBlockOperation off the main thread?
  3. Is everything executed in refreshPostsInPart: off the main thread?
  4. Is the method of my data object, setPosts:withMeta: executed off the main thread?
  5. If I removed the dispatch_async block, would any of the answers to the above questions change?
Community
  • 1
  • 1
Shawn Throop
  • 1,281
  • 1
  • 13
  • 28
  • The execution context of completion handlers must be documented by the asynchronous operation which eventually calls the completion handler. So, you have to look up the documentation and possibly look into the sources, too. AFAIK, the ANK library uses AFNetworking, and that usually invokes its completion handlers on the main thread - despite this isn't the preferred approach. So, "likely" the JSON gets parsed on the main thread - but sources will tell the actual fact. – CouchDeveloper Feb 26 '14 at 17:39
  • Hint: given your scenario I would utilize a third party library which greatly simplifies asynchronous programming problems, like yours. I would suggest [RXPromise](https://github.com/couchdeveloper/RXPromise) (see GitHub) - but I'm biased since I'm the author ;) There are other libraries, too. (Due to time constraints, I can't reply with an thorough answer, perhaps later) – CouchDeveloper Feb 26 '14 at 17:44

3 Answers3

1

From the perspective of someone who's done a lot of network development and written a book on it (http://www.amazon.com/Professional-iOS-Network-Programming-Connecting/dp/1118362403) this code seems overly complex for what you're trying to do. In general, when I start layering concurrency APIs, alarms start going off.

First off: you don't need the dispatch_async block in reload posts. That code will happen very quickly and the queue work should happen on a background thread. I say should because without seeing how the queue is created I won't say for sure if it is background or not.

The semaphore operations are suspect in my mind also. The NSMutableArray is not thread safe, but there are better ways to protect it. Wrap the addObjectFromArray in a @synchronized(posts) { ... } block, that will simplify things a lot.

In situations where I've got unexplained UI stuttering due to blocking, I use Instruments to watch what's going on when this is happening and see what code is actually running on the main thread. Once I identify what code is on the main thread, or blocking something on the main thread, then I back into an answer to why that code is on the main thread.

Jack Cox
  • 3,290
  • 24
  • 25
  • Thanks for the advice. I'm going back into Instruments to recheck where it's being blocked. From a beginner to and expert, is there a better way to see when something is blocking the main thread than looking at the graph in the Time Profiler? And how do you see if code is on the main thread? Instuments is a wild beast to me... – Shawn Throop Feb 26 '14 at 18:02
  • Actually after an hour and a bit of snooping, I've discovered out how to get to some data and see that AFNetworking is calling completion block on the Main Thread – Shawn Throop Feb 26 '14 at 20:31
  • Nice catch. That's probably what most developers want, but if you're post-processing a request that could cause some jankiness in the UI. – Jack Cox Feb 26 '14 at 21:20
  • After some more digging and some helpful tips from App.net, I discovered AFHTTPRequestOperation's `successCallbackQueue` and `failureCallbackQueue`. After fixing self.apiCallMaker to return an instance of ANKJSONRequestOperation (a subclass of AFHTTPRequestOperation) I capture it (probably the wrong terminology) and set its queue to an instance variable/property of the data controller. Anything in the completion block executes off the main thread and my interface never stutters. I've also removed the added `dispatch_async` call as you recommended. It was a futile attempt to tame the madness ;) – Shawn Throop Feb 26 '14 at 23:21
0

So, after some more experimentation with Instruments and research into AFNetworking, I've discovered that the success and failure blocks AFNetworking calls run on the main thread. However, if you have access to the AFHTTPRequestOperation fetching your data, you can set the GCD queues that the success and failure blocks execute on.

In my question, I mentioned another question on Stack Overflow. Now, thanks to @berg (on App.net) apiCallMaker returns an instance of ANKJSONRequestOperation (a subclass of AFHTTPRequestOperation) that I can capture. Here is the code:

// in init
self.callbackQueue = dispatch_queue_create("com.Prose.fetchCallbackQueue", NULL);


- (NSDictionary *)refreshPostsInPart:(StreamPart)part
    ...

    while ((isMore) && (!err)) {

        ANKJSONRequestOperation *op = self.apiCallMaker(client, ^(id responseObject, ANKAPIResponseMeta *meta, NSError *error){
            if (!error) {
                if (!originalMaxID) {
                    originalMaxID = meta.maxID;
                }

                m = meta;
                [posts addObjectsFromArray:(NSArray *)responseObject];
                client.pagination.beforeID = meta.minID;
                isMore = meta.moreDataAvailable;

            } else {
                err = error;
            }

            dispatch_semaphore_signal(semaphore);
        });

        op.successCallbackQueue = self.callbackQueue;

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    }
    ... 
}

Thanks everyone for your input.

Community
  • 1
  • 1
Shawn Throop
  • 1,281
  • 1
  • 13
  • 28
  • The method `refreshPostsInPart` is still _synchronous_ while it should be _asynchronous_. ;) `self.apiCallMaker` seems unnecessary complex, IMHO. And yes, AFNetworking dispatches handlers on the main thread (as stated in my comment). You can check that more easily when setting a breakpoint into handler code, and take a look at the thread view in the left pane of the debugger (in the navigation area). – CouchDeveloper Feb 27 '14 at 00:14
0

I'm trying to give a solution.

First off, your code as it stands now will not run. Furthermore, although I think I have understood the principal problem, I'm not fully understanding the details - in particular all parameters and also how ANK works. Nonetheless, these details seem to be irrelevant for an example solution.

The Problem:

So, it seems you repeatedly have to invoke an asynchronous task which shall execute in serial (not in parallel), thereby consuming data from a stream until it reaches the end. Furthermore, you want to execute completion handlers on a well known execution context (say, the main thread, or a particular queue).

Additionally, it occurs to me, both your methods reloadPosts and refreshPostsInPart are asynchronous.

Note:

  • Each asynchronous method or operation shall have a means to signal completion to the call-site. In many cases this is realized with a completion handler, but there are other approaches, too.

  • A method invoking another method or operation which is asynchronous becomes itself asynchronous. Otherwise, if the calling method is synchronous this is an indication of a code smell.

So, taking a look at your methods:

  1. reloadPosts

    • It does not have a completion hander :(
    • Strangely, it seems it incorporates the asynchronous task AND the completion handler.
  2. refreshPostsInPart

    • It does not have a completion handler. Uhm, stop - it isn't asynchronous! But wait, it actually is asynchronous but you forced it to become synchronous through using a semaphore that blocks the call-site's thread until it is finished: big code smell :)

A Possible Solution:

Luckily, your code can be restructured and greatly simplified.

First, a generic completion handler could be declared as follows:

typedef void (^completion_block_t)(id result, NSError* error);

Your method refreshPostsInPart basically should implement the above Problem. WHEN the task is finished, it should signal the call-site this event through calling the completion handler. The call-site provides the completion handler and defines WHAT to do when that happens:

- (void) refreshPostsInPart:(StreamPart*)part completion:(completion_block_t);

Note: there is no return value! However, you get the result from the completion handler.

Now, since it is unclear to me what you actually want to accomplish, the subsequent solution becomes abstract - but I hope you know how you can "map" this abstract solution to your actual problem.

Furthermore, I'm going to use a third party library which implements the concept of a "Promise". A promise represents the eventual result of an asynchronous task - including an error if that happens. A promise is just another approach to signal completion (or error) to the call-site:

- (RXPromise*)refreshPostsInPart:(StreamPart*)part;

Note, there is no completion handler, instead the method returns a "Promise". The method is actually asynchronous and returns immediately as usual that is a Promise object. Though the promise is not yet "resolved" - that is, it doesn't contain a result yet - even not an error. The promise MSUT be eventually resolved by the asynchronous task. Once that happened, a call-site can obtain the result of the task.

With utilizing promises, you can remove your NSOperationQueues, NSOperations and also the blocks in your solution. RXPromise supports cancellation (not shown in this example) and Promises do also have a much more powerful concept to "chain" operations (also not shown in every detail here in this example). I would appreciate if you read the docs, and possibly consult the web for further information ;)

see also wiki: Futures and Promises

When that promise gets eventually resolved by the asynchronous task it has been either fulfilled with the result or rejected with an error. Then your call-site likely want to do something with the result. You accomplish this with registering success and failure handlers:

[self refreshPostsInPart:part]
.then(^id(id result){
    // On Success:
    NSLog(@"This is the result: %@", result);
    return nil;
}, ^id (NSError* error){
    // On Failure:
    NSLog(@"Error: %@", error);
    return nil;
});

That is, you "setup" your success and/or failure handler with that then expression:

[self refreshPostsInPart:part].then(<success-handler>, <failure-handler>); 

which is the short form of:

RXPromise* resultPromise = [self refreshPostsInPart:part];
resultPromise.then(<success-handler>, <error-handler>);

Note that - here - the completion handlers will be executed on some private execution context. However, in RXPromise you can explicitly define what queue to use where the handlers get executed, using thenOn, for example:

resultPromise.thenOn(dispatch_queue_get_main_queue(), 
^id(id result){
    // do something with result on the main queue
    return nil;
}, nil /*error handler not used*/);

Now, we can take a look at the implementation of the method refreshPostsInPart::

Suppose, your class StreamPart will be represented as a NSArray in this solution which mimics a rather simple "stream" of objects.

The solution also utilizes a class method from the RXPromise library repeat which greatly simplifies an "asynchronous loop":

repeat is declared as follows:

typedef RXPromise* (^rxp_nullary_task)();

+ (RXPromise*) repeat:(rxp_nullary_task)block;

It asynchronously executes the block in a continuous loop until the block returns nil or the returned promise will be rejected.

Note, repeat is itself asynchronous, thus it returns a promise.

So a canonical implementation for a repeat would look like:

RXPromise* allFinishedPromise = [RXPromise repeat:^RXPromise*{
    if (no more input) {
        return nil;  // terminate the asynchronous loop
    }
    return [input asyncTask];  // asynchronously execute the next task with input
}];

and possible usage is:

allFinishedPromise.then(^id(id result){
    // in case of success, the result of repeat is always @"OK" 
}, 
^id(NSError* error){
    // if an error occurred, this is the error of the failing task
});

So, we also need that "task" which is basically the code which executes per iteration, and this is an asynchronous task (thus, it returns a promise):

-(RXPromise*) asyncTask;

This basically implements your reloadPosts. I'll omit this implementation for now.

Note that your original implementation contained the completion handler code. We do that not. Instead we register a success and failure handler as usual.

Furthermore you likely want to continue with some particular code which gets executed thereafter once a handler has been finished. Maybe your handler itself is an asynchronous method! You can do such things in a concise manner, for example:

Suppose, you have implemented asyncTask (which basically implements the task part of your reloadPosts method), and then when that finished want to execute another asynchronous method, and then when that also finished want to print the result:

[self asyncTask]
.then(^id (id result){
    // take result of asyncTask and pass it through another asynchronous task (which returns a Promise:
    return [self anotherAsyncTaskWithInput:result];
}, nil)
.then(^id (id result) {
     // here: result is the result of "anotherAsyncTaskWithInput:"
     NSLog(@"Final Result: %@", result);
}, nil);

Now, that you know how to "chain" asynchronous tasks, we can finally write the sample implementation of the method refreshPostsInPart::

- (RXPromise*) refreshPostsWithArray(NSArray* inputs)
{
    const NSUInteger count = [inputs count];
    __block NSUInteger i = 0;
    return [RXPromise repeat:^RXPromise*{
        // Check termination condition:
        if (i >= count) {
            return nil;  // stream at EOS
        }
        RXPromise* finalResult = [inputs[i++] asyncTask]
        .then(^id(id result){
            // do something with result (that is your completion handler part of `reloadPosts `
            return nil;  // note: intermediate result will not be used in repeat!
        }, nil);
        return finalResult;
    }];
}

Note: I'm taking an NSArray as example for a StreamPart. We just need to know when the input is consumed, so that we can return nil (instead a Promise) in order to terminate the repeat loop. I think you know how to map this to your StreamPart class.

Finally, I copy a functional Foundation console example. This requires to link against the RXPromise library. Feel free to experiment with it.

Running console sample

#import <Foundation/Foundation.h>
#import <RXPromise/RXPromise.h>
#import <RXPromise/RXPromise+RXExtension.h>

// As an example add a category for NSString to simulate an asynchronous task
@implementation NSString (Example)

- (RXPromise*) asyncTask
{
    RXPromise* promise = [[RXPromise alloc] init];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        int count = 10;
        while (count--) {
            usleep(100*1000);
        }
        if ([self isEqualToString:@"X"]) {
            [promise rejectWithReason:@"Bad Input"];
        }
        else {
            [promise fulfillWithValue:[self capitalizedString]];
        }
    });
    
    return promise;
}

@end


RXPromise* performTasksWithArray(NSArray* inputs)
{
    const NSUInteger count = [inputs count];
    __block NSUInteger i = 0;
    return [RXPromise repeat:^RXPromise*{
        if (i >= count) {
            return nil;
        }
        return [inputs[i++] asyncTask].then(^id(id result){
            NSLog(@"%@", result);
            return nil;  // intermediate result not used in repeat
        }, nil);
    }];
}


int main(int argc, const char * argv[])
{
    @autoreleasepool {
        
        NSArray* inputs = @[@"a", @"b", @"c", @"X", @"e", @"f", @"g"];
        
        [performTasksWithArray(inputs)
        .then(^id(id result){
            NSLog(@"Finished: %@", result);
            return nil;
        }, ^id(NSError* error){
            NSLog(@"Error occured: %@", error);
            return nil;
        }) runLoopWait];
        
    }
    return 0;
}

Note: the current input simulates an error: @"X" causes the asyncTask to fail!

Console log:

2014-02-26 23:06:44.412 Sample7[1410:1003] A
2014-02-26 23:06:45.422 Sample7[1410:2403] B
2014-02-26 23:06:46.434 Sample7[1410:1003] C
2014-02-26 23:06:47.578 Sample7[1410:2403] Error occured: Error Domain=RXPromise Code=-1000 "The operation couldn’t be completed. Bad Input" UserInfo=0x10010ba00 {NSLocalizedFailureReason=Bad Input}
Community
  • 1
  • 1
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • Wow, thank you for this very detailed solution. I feel bad because I should have made a few things more clear. I'm not a fan of using third party libraries, I like to know my code and what is happening. My code does run, and I don't mind that somethings are synchronous, In fact reloadPostsInPart is supposed to be. I load up a queue with requests for different stream parts, wait for them to finish, and then return that value to my data object with `setPosts:withMeta:`. The data object processes and saves the data. Via a delegate, it hands proper data to the tableView that called reloadPosts. – Shawn Throop Feb 27 '14 at 15:49
  • I understand my code isn't elegant, and in the current context probably confusing and incorrect. I'm sorry for that. I also understand there are better ways to do what I want. I'm going to look into RXPromises, but at a later date. Right now, I'm learning and trying to get this app out the door. Thanks again for the detailed answer, I'm going to read it a few times more to fully grok it and I'll revisit it in the future. – Shawn Throop Feb 27 '14 at 15:56