dispatch_async Does Not Run - ios

I'm new to queues and threading. Below I have the following code that seeks to run various methods within an asynchronous queue. Each item in the queue will update a count when finished, and when all the items are done in the queue, the update count will be complete and ready for return.
The problem is that when this code runs the last line is called and then, it hangs with no errors and I cannot continue to step through the trace.
__block int masterUpdateCount = 0;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self updateStuffWithCompletionCount:^(int updatedItemsCount) {
masterUpdateCount += updatedItemsCount;
}];
[self updateMoreStuffWithCompletionCount:^(int updatedItemsCount) {
masterUpdateCount += updatedItemsCount; // Never gets called
}];
}); // <------ APPLICATION HANGS HERE after calling dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
return masterUpdateCount;
Something I noticed is that the completion blocks never get called, which could very well be why it hangs forever, but my question is why? If it helps, inside the updateStuffWithCompletionCount type methods I'm actually initializing an NSURLSession with an NSURLSessionDataTask and I am in fact running the task by calling [task resume];, so I don't see why the completion wouldn't be called.
Here is what it looks like inside the updateStuffWithCompletionCount method:
- (void) updateStuffWithCompletionCount: (void (^)(int)) completionResult
{
__block int updateCount = 0;
NSString *someURL = #"www.someplace.com";
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDataTask * getDataTask = [defaultSession dataTaskWithURL:[NSURL someURL] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if(error == nil)
{
// do stuff, if updated, add to count
updateCount ++;
if(updateCount > 0) {
completionResult(updateCount); // Invoke the completion handler
} else {
completionResult(0); // Invoke the completion handler
}
}
else {
NSLog(#"Error: %#", error);
completionResult(-1);
}
}];
[getDataTask resume];
}
Hopefully an experienced eye can point it out. Appreciated.

Include code for your dataTask please. This line masterUpdateCount += updatedItemsCount; // Never gets called , because its in the completion block which means it doesn't get ran until your datatask is complete, Did you set a breakpoint within your datatask and make sure something its jumping in there? Do you have a NSLog(%#, error.description) line in there in case your getting one? Looks like you may need to do more debugging there.

Related

NSOperationQueue currentQueue not working

I am trying to run AFURLConnectionOperation seen below on the currentQueue as I want to keep my main thread free for user interatoin, however nothing happens when I call mainQeue.
However if I call the same AFURLConnectionOperation on mainQueue it works perfectly.
Pleas see following code
// Send Batch
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"%lu of %lu complete", (unsigned long)numberOfFinishedOperations, (unsigned long)totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
// check batch response
NSError *error;
for (AFHTTPRequestOperation *op in operations) {
if (op.isCancelled){
return ;
}
if (op.responseObject){
// Current JSON Batch complete
NSMutableArray *jsonObject = [NSJSONSerialization JSONObjectWithData:op.request.HTTPBody options:kNilOptions error:&error];
// Update sent_flag using current chunk
[[SyncModel sharedInstance] updateSentFlag:jsonObject];
}
if (op.error){
error = op.error;
NSLog(#"Error == %#", error);
}
}
}];
Then finally I call one or the other of the following code
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO]; // this works
[[NSOperationQueue currentQueue] addOperations:operations waitUntilFinished:NO]; // this dose not work
The reason is
You can use this method from within a running operation object to get a reference to the operation queue that started it. Calling this method from outside the context of a running operation typically results in nil being returned.
So,I guess,if you log [NSOperationQueue currentQueue],it is nil
If you want a new queue,use
[[NSOperationQueue alloc] init];
After adding the operation on queue, if the operation doesn't start eventually then there are two ways to get them executed.
Using wait block, for example during unit test using XCTest framework, use
XCTestExpectation *expectation1 = [self expectationWithDescription:#"ExtractColorsInternal function call on NSOperationQueue"];
dispatch_async(dispatch_get_main_queue(), ^{
[expectation1 fulfill];
});
[self waitForExpectationsWithTimeout:1000 handler:^(NSError *error) {
if (error != nil) {
NSLog(#"Error: %#", error.localizedDescription);
}
}];
call CFRunLoopRun(), which would execute the present operation in current queue succesfully

NSURLSession delegates on UI thread with NSOperation

I got challenged to use the NSURLSession delegates to update a UI element (just a label) with the status of the download, but I can only use NSOperation and not dispatch_get_main_queue
I'm not even sure this is possible to call the UI thread with NSOperation directly (and not a completion block), but I thought of ask here to see if anyone knows if this is possible.
In a nutshell, this is what I have with the C calls to dispatch_async:
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
long percentage = (totalBytesWritten * 100) / totalBytesExpectedToWrite;
dispatch_async(dispatch_get_main_queue(),
^{
self.label_bytesExpected.text = [NSString stringWithFormat:#"Expected = %lld", totalBytesExpectedToWrite];
self.label_bytesWritten.text = [NSString stringWithFormat:#"Received = %lld (%ld%%)", totalBytesWritten, percentage];
});
}
Is it possible to call NSOperation (queue or block or anything else with it) to display this on the UI instead of using dispatch_async(dispatch_get_main_queue(), block)?
Thank you!
Yes. It is possible. You should use [NSOperationQueue mainQueue]
+ (NSOperationQueue *)mainQueue
Returns the operation queue associated with the main thread.
So your code could be:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.label_bytesExpected.text = [NSString stringWithFormat:#"Expected = %lld", totalBytesExpectedToWrite];
self.label_bytesWritten.text = [NSString stringWithFormat:#"Received = %lld (%ld%%)", totalBytesWritten, percentage];
}];
NSURLSession has a property called delegateQueue which indicates on which queue delegate's methods will be called. It can be set to [NSOperationQueue mainQueue] via factory method:
[NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:[NSOperationQueue mainQueue]];

NSURLSessionDataTask and handler block

I'm developing a library that gets json data from a server, and I'm using NSURLSessionDataTask. In order to test my library I created a new project that calls this library method.
typedef void (^CompletionBlock)();
// More stuff...
- (void)downloadAllPodcastsMetadataWithCompletionHandler:(CompletionBlock)block{
NSURL *url = [NSURL URLWithString:#"MyServerURL"];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Connection and response handling stuff...
// When my data is saved, executes the block parameter
dispatch_async(dispatch_get_main_queue(), ^{
block();
});
}
[dataTask resume];
}
EDIT: I forgot to put [dataTask resume] here, but it's in my code. Sorry.
EDIT 2: So, as some of you have said, i changed dispatch_sync with dispatch_async. But the result is the same :(
In my test project i call this method like this.
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[manager downloadAllPodcastsMetadataWithCompletionHandler:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}];
But the network activity Indicator never shows off. It's like the block parameter never executes.
It's because it executes the NSURLSessionDataTask logic inside a library and then I should use something else instead dispatch_sync?
I already checked NSURLSessionDataTask not executing the completion handler block and I think I do the same. If it helps, manager is a Singleton. Any thoughts?
Thank you.
You need to start the download with [dataTask resume];.
You need to add [dataTask resume]; and also add this in completion handler
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
});
I tested some code.
Dispatch_sync won't work.
dispatch_sync(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
});
Dispatch_async worked for me. But I had to put the code on veiwWillAppear.
Not sure what's going to happen using it inside a block...
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
});
Well, i'm dumb.
There was an error in my previous code i didn't notice, so the completion block war never executing. That is what happens when someone has too much confidence in his code.
Sorry for bothering you.

Best practices for making a queue of NSURLSessionTasks

What are the best practices for making a serial queue of NSURLSessionTasks ?
In my case, I need to:
Fetch a URL inside a JSON file (NSURLSessionDataTask)
Download the file at that URL (NSURLSessionDownloadTask)
Here’s what I have so far:
session = [NSURLSession sharedSession];
//Download the JSON:
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task =
[session dataTaskWithRequest:dataRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//Figure out the URL of the file I want to download:
NSJSONSerialization *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSURL *downloadURL = [NSURL urlWithString:[json objectForKey:#"download_url"]];
NSURLSessionDownloadTask *fileDownloadTask =
[session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:playlistURL]]
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(#"completed!");
}];
[fileDownloadTask resume];
}
];
Apart from the fact that writing a completion block within another completion looks messy, I am getting an EXC_BAD_ACCESS error when I call [fileDownloadTask resume]... Even though fileDownloadTask is not nil!
So, what is the best of way of sequencing NSURLSessionTasks?
You need to use this approach which is the most straight forward: https://stackoverflow.com/a/31386206/2308258
Or use an operation queue and make the tasks dependent on each others
=======================================================================
Regarding the HTTPMaximumConnectionsPerHost method
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1
HTTPMaximumConnectionsPerHost only ensure that one shared connection will be used for the tasks of that session but it does not mean that they will be processed serially.
You can verify that on the network level using http://www.charlesproxy.com/, you wil discover that when setting HTTPMaximumConnectionsPerHost, your tasks will be still be started together at the same time by NSURLSession and not serially as believed.
Expriment 1:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
With task1: url = download.thinkbroadband.com/20MB.zip
With task2: url = download.thinkbroadband.com/20MB.zip
calling [task1 resume];
calling [task2 resume];
Result: task1 completionBlock is called then task2 completionBlock is called
The completion blocks might be called in the order you expected in case the tasks take the same amount of time however if you try to download two different thing using the same NSURLSession you will discover that NSURLSession does not have any underlying ordering of your calls but only completes whatever finishes first.
Expriment 2:
Declaring a NSURLSession with HTTPMaximumConnectionsPerHost to 1
task1: url = download.thinkbroadband.com/20MB.zip
task2: url = download.thinkbroadband.com/10MB.zip (smaller file)
calling [task1 resume];
calling [task2 resume];
Result: task2 completionBlock is called then task1 completionBlock is called
In conclusion you need to do the ordering yourself, NSURLSession does not have any logic about ordering requests it will just call the completionBlock of whatever finishes first even when setting the maximum number of connections per host to 1
PS: Sorry for the format of the post I do not have enough reputation to post screenshots.
Edit:
As mataejoon has pointed out, setting HTTPMaximumConnectionsPerHost to 1 will not guarantee that the connections are processed serially. Try a different approach (as in my original answer bellow) if you need a reliable serial queue of NSURLSessionTask.
An easy way to implement a first-in first-out serial queue of NSURLSessionTasks is to run all tasks on a NSURLSession that has its HTTPMaximumConnectionsPerHost property set to 1:
+ (NSURLSession *)session
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
[configuration setHTTPMaximumConnectionsPerHost:1];
session = [NSURLSession sessionWithConfiguration:configuration];
});
return session;
}
then add tasks to it in the order you want.
NSURLSessionDataTask *sizeTask =
[[[self class] session] dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
#import "SessionTaskQueue.h"
#interface SessionTaskQueue ()
#property (nonatomic, strong) NSMutableArray * sessionTasks;
#property (nonatomic, strong) NSURLSessionTask * currentTask;
#end
#implementation SessionTaskQueue
- (instancetype)init {
self = [super init];
if (self) {
self.sessionTasks = [[NSMutableArray alloc] initWithCapacity:15];
}
return self;
}
- (void)addSessionTask:(NSURLSessionTask *)sessionTask {
[self.sessionTasks addObject:sessionTask];
[self resume];
}
// call in the completion block of the sessionTask
- (void)sessionTaskFinished:(NSURLSessionTask *)sessionTask {
self.currentTask = nil;
[self resume];
}
- (void)resume {
if (self.currentTask) {
return;
}
self.currentTask = [self.sessionTasks firstObject];
if (self.currentTask) {
[self.sessionTasks removeObjectAtIndex:0];
[self.currentTask resume];
}
}
#end
and use like this
__block __weak NSURLSessionTask * wsessionTask;
use_wself();
wsessionTask = [[CommonServices shared] doSomeStuffWithCompletion:^(NSError * _Nullable error) {
use_sself();
[self.sessionTaskQueue sessionTaskFinished:wsessionTask];
...
}];
[self.sessionTaskQueue addSessionTask:wsessionTask];
I use NSOperationQueue (as Owen has suggested). Put the NSURLSessionTasks in NSOperation subclasses and set any dependancies. Dependent tasks will wait until the task they are dependent on is completed before running but will not check the status (success or failure) so add some logic to control the process.
In my case, the first task checks if the user has a valid account and creates one if necessary. In the first task I update a NSUserDefault value to indicate the account is valid (or there is an error). The second task checks the NSUserDefault value and if all OK uses the user credentials to post some data to the server.
(Sticking the NSURLSessionTasks in separate NSOperation subclasses also made my code easier to navigate)
Add the NSOperation subclasses to the NSOperationQueue and set any dependencies:
NSOperationQueue *ftfQueue = [NSOperationQueue new];
FTFCreateAccount *createFTFAccount = [[FTFCreateAccount alloc]init];
[createFTFAccount setUserid:#"********"]; // Userid to be checked / created
[ftfQueue addOperation:createFTFAccount];
FTFPostRoute *postFTFRoute = [[FTFPostRoute alloc]init];
[postFTFRoute addDependency:createFTFAccount];
[ftfQueue addOperation:postFTFRoute];
In the first NSOperation subclass checks if account exists on server:
#implementation FTFCreateAccount
{
NSString *_accountCreationStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"CHECKING" forKey:ftfAccountStatusKey];
// Setup and Run the NSURLSessionTask
[self createFTFAccount:[self userid]];
// Hold it here until the SessionTask completion handler updates the _accountCreationStatus
// Or the process takes too long (possible connection error)
while ((!_accountCreationStatus) && (timeElapsed < 5.0)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
}
if ([_accountCreationStatus isEqualToString:#"CONNECTION PROBLEM"] || !_accountCreationStatus) [self cancel];
if ([self isCancelled]) {
NSLog(#"DEBUG FTFCreateAccount Cancelled" );
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:#"ERROR" forKey:ftfAccountStatusKey];
}
}
In the next NSOperation post data:
#implementation FTFPostRoute
{
NSString *_routePostStatus;
}
- (void)main {
NSDate *startDate = [[NSDate alloc] init];
float timeElapsed;
NSString *ftfAccountStatusKey = #"ftfAccountStatus";
NSString *ftfAccountStatus = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:ftfAccountStatusKey];
if ([ftfAccountStatus isEqualToString:#"ERROR"])
{
// There was a ERROR in creating / accessing the user account. Cancel the post
[self cancel];
} else
{
// Call method to setup and run json post
// Hold it here until a reply comes back from the operation
while ((!_routePostStatus) && (timeElapsed < 3)) {
NSDate *currentDate = [[NSDate alloc] init];
timeElapsed = [currentDate timeIntervalSinceDate:startDate];
NSLog(#"FTFPostRoute time elapsed: %f", timeElapsed);
}
}
if ([self isCancelled]) {
NSLog(#"FTFPostRoute operation cancelled");
}
}

AFNetworking background JSON parsing avoid block nesting

I have some networking code with heavy JSON parsing going on. It needs to be done in the background to not block the main thread. The code looks like this :
-(void) getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
// sometimes I have more requests
// startOperations is a wrapper on AFHTTPClient enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock:
// that handles errors and loading views
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
// getBgQueue = return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
[self.localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
// this is executed on main thread
if(completion) completion(...);
}];
});
}];
}
(AFNetworking 1.x)
The above code works very fine, but it's a pain to setup and write. And often the whole method content is wrapped inside another block to fetch some required data first... basically the blocks just pile up and makes ugly code
I'm using enqueueBatchOfHTTPRequestOperations and not individual completion blocks on AFJSONRequestOperation because batch completion block would sometimes fire before all individual operations completion blocks... (I also read somewhere that Mattt discouraged doing this)
Any pointers on how to do better than this?
I'm not sure what you want here, but just like "longcat is long", it's somewhat inherent in the pattern: 'continuation-passing style is continuation-passing style'. If you want to flatten things out a bit, you could make local block variables, but to a certain degree, you're stuck because you need the completion for -MR_saveToPersistentStoreWithCompletion to close over data in order to pass it to the -getSomeDataWithParameters... completion, but data won't exist until the -startOperations completion is executed.
You could probably achieve a less-nested appearance by using a bunch of __block variables, and splitting the code into several local blocks, but to me that feels kind of like cutting off your nose to spite your face. This code is readily understandable the way it is.
By the way... I notice that you're closing over op in the -startOperations completion block. This is fine because you're enqueuing op by doing -startOperations: #[op] ... but it would arguably be cleaner to get op from the operations parameter to the completion. I tightened this up as much as seemed reasonable:
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = completion ? ^(BOOL success, NSError *error) { completion(data); } : nil;
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}
}];
}
This will fan out each response potentially to a different thread. If you want all responses to execute on a single background thread, just swap the nesting of the for loop and the dispatch_async.
From there, the only really "superfluous" code is the dispatch_async. You could eliminate that by making -startOperations:... take a queue parameter where you would pass in the queue you wanted the completion to be called. Maybe like this:
- (void)startOperations: (NSArray*)ops completionQueue: (dispatch_queue_t)queue completionBlock: (void (^)(NSArray*))completion
{
void (^completionWrapper)(NSArray*) = !completion ? nil : ^(NSArray* ops) {
if (queue)
dispatch_async(queue, ^{ completion(ops); });
else
completion(ops);
};
[self startOperations: ops completionBlock: completionWrapper];
}
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionQueue: getBgQueue() completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = !completion ? nil : ^(BOOL success, NSError *error) { completion(data); };
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}];
}

Resources