John Verrone
John Verrone

Reputation: 307

viewDidLoad multithreading issue

I have to make a separate api call that returns some JSON data for each movie that I want to get information about. I am trying to loop through the array of movie IDs and call populateAssetObject on each one of them in my viewDidLoad method.

If I go into debugging mode and step through the for loop, it will populate movies correctly with all 5 titles, but if I run it normally, my movies array is only the first 2 objects. I think this may be caused by some multithreading? I'm not really an expert in that area, does anyone know what my problem could be?

viewDidLoad:

_movies = [[NSMutableArray alloc] init];

for (NSString *curr in assetIDs) {
    [self populateAssetObject:curr];
}

here is the populateAssetObject method

-(void)populateAssetObject:(NSString *)videoID {
    NSString *urlString = [NSString stringWithFormat:@"[api url]", videoID];
    NSURL *url = [NSURL URLWithString:restURLString];

    NSData *data = [[NSData alloc] initWithContentsOfURL:url];
    NSError *error = nil;

    NSDictionary *contents = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];

    OVDAsset *newAsset = [[OVDAsset alloc] init];
    [newAsset setTitle:[contents valueForKey:@"title"]];
    [newAsset setDescription:[contents valueForKey:@"longDescription"]];

    [self.movies addObject:newAsset];
}

Upvotes: 1

Views: 1141

Answers (1)

CouchDeveloper
CouchDeveloper

Reputation: 19116

Your approach has an important issue:

Your method populateAssetObject: is a synchronous method and it will access remote resources. This method will be executed on the main thread, and thus this blocks the UI.

That you invoke it in a loop, makes the situation only worse.

What you really would need is an asynchronous method which performs all that in a background thread, and a completion block which notifies the call-site when the whole operation is finished:

typedef void (^completion_t)();
- (void) populateAssetsWithURLs:(NSArray*) urls completion:(completion_t)completionHandler;

In your viewDidLoad you would then do this:

- (void) viewDidLoad {
    [super viewDidLoad];
    [self populateAssetsWithURLs:urls ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });
    }];
}

The tricky part is to implement the method populateAssetsWithURLs:completion:.

A quick and dirty approach which look like this:

- (void) populateAssetsWithURLs:(NSArray*) urls
                     completion:(completion_t)completionHandler
{
    NSUInteger count = [urls count];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSUInteger i = 0; i < count; ++i) {
            [self synchronousPopulateAssetObject:urls[i]];
        }
        if (completionHandler) {
            completionHandler();
        }
    });
}

- (void) synchronousPopulateAssetObject:(NSString*)url {
   ...
}

This approach has a few issues:

It blocks at least one thread (but likely more) just for waiting on the result. This approach is inefficient regarding system resources - but it may work.

Asynchronous Loop

A better approach would employ an asynchronous design. The tricky part with this approach is that you have a list of asynchronous tasks (asynchronousPopulateAssetObject), each one needs to be started asynchronously and the final result is only available asynchronously as well. Having a for loop is quite inappropriate for making a loop asynchronous.

Thus, you may imagine an API like this, which could be a Category of NSArray:

Category NSArray:

typedef void (^completion_t)(id result);

-(void) forEachPerformTask:(task_t)task completion:(completion_t)completionHandler;

Note, that task is an asynchronous block of type task_t, having its own completion handler as a parameter:

typedef void (^task_t)(id input, completion_t);

The task will be asynchronously applied for each element in the array. When all elements have been processed, the client will be notified by calling the completion handler passed in method forEachPerformTask.

You can find a complete implementation and a short sample here on GitHub Gist: transform_each.m.

Shortly, I'll edit my answer and demonstrate a much more elegant approach, which utilizes a helper library, especially suited to solve asynchronous patterns like this one.

But before that I will just demonstrate another approach utilizing NSOperationQueue:

NSOperationQueue

NSOperationQueue has the invaluable advantage that running and pending asynchronous tasks can be cancelled. In fact, an asynchronous solution which executes a list of operations which cannot be cancelled is an incomplete solution and likely not suitable at all in most scenarios.

Furthermore, NSOperationQueue can execute its tasks concurrently. The number of the occurrent tasks can be set with property maxConcurrentOperationCount.

There are a few variations when using a NSOperationQueue and the basic idea of the most simple one would be:

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;

for (NSString* url in urls) {
    [queue addOperationWithBlock:^{
        [self synchronousPopulateAssetObject:url];
    }];
}

Here, the operation queue is configured to run two tasks in parallel. An operation will be created using the convenient method addOperationWithBlock: which creates an NSOperation object on the fly from a block.

The tasks will be enqueued at at once in the for loop. This is different to the "dispatch approach" whose implementation is shown on Gist. In the the "dispatch approach" a new task will be enqueued only after the preceding one has been finished. This is quite friendly to system resources.

The disadvantage here is, that one cannot asynchronously determine when all the tasks have been finished. There is a "blocking" solution though, using method waitUntilAllOperationsAreFinished. Since this method blocks the calling thread, and since we want an asynchronous method populateAssetsWithURLs:completion: we need to wrap the synchronous method into an asynchronous one as follows:

- (void) populateAssetsWithURLs:(NSArray*) urls
                     queue:(NSOperationQueue*)queue
                     completion:(completion_t)completionHandler
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSString* url in urls) {
            [queue addOperationWithBlock:^{
                [self synchronousPopulateAssetObject:url];
            }];
        }
        [queue waitUntilAllOperationsAreFinished];
        if (completionHandler) {
            completionHandler();
        }
    });
}

Note: the client provides the queue. This is because the client can send cancelAllOperations to the queue at any time to stop the execution of the pending and running tasks.

The disadvantage here is, that we need an extra thread which gets blocked just for the purpose to deliver the eventual result (which could be passed as a parameter in the completion handler).

Another disadvantage is when using the convenient method addOperationWithBlock: we don't have the opportunity to specify a completion handler for the asynchronous task.

And another disadvantage when using convenient method addOperationWithBlock: is that we don't get the NSOperation which would be required to setup dependencies to other NSOperation objects (see Operation Dependencies in the official documentation of NSOperation).

If we want to leverage the full power of NSOperations and NSOperationQueue we would have to be more elaborate. For example, having a completion handler which notifies the call-site when the operation queue has processed ALL its tasks, is doable - but it requires to setup dependencies and this requires an NSOperation object where we need a subclass, and need to execute the code what was formerly the simple asynchronous task.

Nonetheless, the dependency feature is invaluable and I would strongly recommend to experiment with it.

Upvotes: 1

Related Questions