I have been trying to figure out when it's okay to "just type in what I need done" and when I need to be specific about what kind of work I am doing on what kind of thread.
As I understand I should only update the UI on my main thread. Does this mean that it's not okay to do something like this? Should I put this into a GDC call?
[sessionManager dataTaskWithRequest:aRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
someUILabel.text = #"Hello!"; // Updating my UI
[someTableView reloadData]; // Ask a table view to reload data
}];
That's it for the UI part. Now, let's assume I had an NSMutableArray somewhere in in my class. I would be adding or removing objects to this array by for instance tapping a UIButton. Then again I have a NSURLSessionDataTask going to a server somewhere to get some data and load it into my NSMutableArray, like so:
[sessionManager dataTaskWithRequest:aRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
myMutableArray = [[responseObject objectForKey:#"results"] mutableCopy];
}];
This is not a UI operation. Does this need to be wrapped in a GDC call to avoid crashing in a race condition between my button-tap adding an object (i.e. [myMutableArray insertObject:someObj atIndex:4];) while the completion block runs, or are these designed to not clash into each other?
I have left out all error handling to focus on the question at hand.
TLDR: It costs you nothing to call dispatch_async(dispatch_get_main_queue()... inside your completion handler, so just do it.
Long Answer:
Let's look at the documentation, shall we?
completionHandler The completion handler to call when the load request is complete. This handler is executed on the delegate queue.
The delegate queue is the queue you passed in when you created the NSURLSession with sessionWithConfiguration:delegate:delegateQueue:. If that's not how you created this NSURLSession, then I suggest you make no assumptions about what queue the completion handler is called on. If you didn't pass [NSOperationQueue mainQueue] as this parameter, you are on a background queue and you should break out to the main queue before doing anything that is not thread-safe.
So now the question is:
Is it thread-safe to update the UI and talk to the table view? No, you must do those things only on the main queue.
Is it thread-safe to set myMutableArray? No, because you would then be sharing a property, self.myMutableArray, between two threads (the main queue, where you usually talk to this property, and this queue, whatever it is).
Related
I'm using NSURLSession to make multiple asynchronous requests to my server with following code:
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
...
[self.dict setObject:some_obj forKey:some_key];
}] resume];
Inside the response block I'm setting key/value pairs for an mutable Dictionary.
My question is:
As the requests are asynchronous, can it be, that theoretically my program tries to set key/value pairs for the dictionary at the same time? An if this is possible, what will happen?
Does the app crash?
Will be certain key/value pairs not set?
Or will it work, as one key/value setting will wait for the other to finish?
If 3. is not the case, what can I do to make 3) work?
NSMutableDictionary is not documented as thread-safe, so it almost certainly isn't.
However, the Apple docs on NSURLSession say:
The completion handler to call when the load request is complete. This handler is executed on the delegate queue.
You (may) pass the delegate queue at session creation, the docs say:
An operation queue for scheduling the delegate calls and completion handlers. The queue need not be a serial queue. If nil, the session creates a serial operation queue for performing all delegate method calls and completion handler calls.
So as far as setting the keys, if you don't explicitly create the session with a parallel queue you should be fine. If you did, then you'll need to synchronize access. The easiest way is an #synchronized block:
#synchronized (self.dict) {
self.dict[key] = value;
}
Depending on when and where you're reading the values, you may need the synchronized block anyway.
__block NSHTTPURLResponse *httpResponse;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error)
httpResponse = (NSHTTPURLResponse *)response;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
Is it safe to read httpResponse after this? The semaphore waits for the block to compete execution. If there was no error, will the assignment be seen immediately or do I have to synchronise or create a memory barrier outside the block?
Does waiting on the semaphore implicitly perform some synchronisation which makes the __block variable safe to read immediately. If this was done with Thread.join() in Java instead of a semaphore, it would be safe since it guarantees a happens-before relationship with the assignment in the "block".
The short answer is yes.
The semaphore lock essentially forces the thread that is currently operating to stop execution until it receives enough unlock signals to proceed.
The variable you have defined is modified on some other thread before the semaphore is allowed to continue executing, so your assignment should have safely occurred.
Strictly speaking, this code will block on the executing thread (probably the main thread) until the semaphore lock is signalled. So - short answer, yes it should work, but it isn't best practice because it blocks the main thread.
Longer answer:
Yes, the semaphore will make sure the __block captured storage isn't accessed until it has been filled in. However, the calling thread will be blocked by the wait until the block has completed. This isn't ideal - normal UI tasks like making sure Activity Indicators spin won't happen.
Best practice would be to have the block signal the main object (potentially using a dispatch_async call to the main queue) once it has completed, and only accessing it after that. This is especially true given that if your session task fails (e.g. from network connectivity), then the calling thread will potentially block until the completion handler is called with a timeout error. This will appear to a user like the app has frozen, and they can't really do anything about it but kill the app.
For more information on working with blocks, see:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html
For best practice with Data Session tasks in particular:
http://www.raywenderlich.com/51127/nsurlsession-tutorial
It seems dispatch_semaphore_wait is also a memory barrier, so the value can be safely read.
I have a class set up to handle my web API calls. This is done using an NSMutableURLRequest and an NSRLlConnection. I initially used connectionWithRequest: delegate: and that worked well for the most part, except when I depended on this request being truly asynchronous, not just partially executing in the main run loop.
To do this, I thought I would just use the ever so convenient sendAsynchronousRequest: queue: completionHandler: and at first in all of my unit tests I thought this worked great. It performed asynchronously, my semaphores were waited on and signaled correctly, it was great.
Until I tried to re-use this new modified version of my Web service class in my actual app. Part of my app plays a video and uses a repeating NSTimer to update part of the screen based on the current playback time of the video. For some unknown reason, as long as I have executed at least one of these new asynchronous NSURLConnections both the video playback and the timer no longer work.
Here is how I initialize the connection:
[NSURLConnection sendAsynchronousRequest:requestMessage
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
if ( data.length > 0 && connectionError == nil )
{
_webServiceData = [data mutableCopy];
[self performSelector:#selector(connectionDidFinishLoading:) withObject:nil];
}
else if ( connectionError != nil )
{
_webServiceData = [data mutableCopy];
[self performSelector:#selector(webServiceDidFinishExecutingWithError:) withObject:connectionError];
}
}];
Here is how I initialize my repeating timer:
playbackTimeTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(checkPlaybackTime) userInfo:nil repeats:YES];
And I have absolutely no idea why the asynchronous NSURLConnection is causing aspects of my app that are completely unrelated to stop functioning.
EDIT:
For clarification I have ViewControllerA that performs the web requests to retrieve some data. When that data is successfully retrieved, ViewControllerA automatically segues to ViewControllerB. In ViewControllerB's viewWillAppear is where I set up my movie player and timer.
In the future, do not use semaphores to make the test wait. Use the new XCTestExpectation, which is designed for testing asynchronous processes.
And unlike the traditional semaphore trick, using the test expectation doesn't block the main thread, so if you have completion blocks or delegates that require the main thread, you can do this in conjunction with the test expectation.
You're clearly doing something that isn't working because you're running this on the background queue. Typical problems include
If you were trying to run that timer from the background thread, that wouldn't work, unless you used one of the standard timer workarounds (scheduling it on main runloop, dispatching the creation of the timer back to main thread, using dispatch timer, etc.).
Having earlier described these alternatives in great detail, it turns out you're initiating the timer from viewWillAppear, so that's all academic.
any UI updates (including performing segues, reloading tables, etc.).
care should be taken when synchronizing class properties updated from background thread (these might best be dispatched to main thread, too).
Anyway, you might remedy this by just tell sendAsynchronousRequest to run its completion block on the main queue:
[NSURLConnection sendAsynchronousRequest:requestMessage
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
...
}];
Then, the completion block would have run on the background queue, and everything would probably be fine. Or you can manually dispatch the calling of your completion handlers back to the main queue.
But make sure that any UI updates (including the performing a segue programmatically) are run on the main thread (either by running the whole completion block on the main thread, or manually dispatching the relevant calls to the main thread).
I think I am on the right track, but just wanted to double check here. I recently started using AFNetworking to obtain a large XML file from a database, which I then need to parse (I got that part all figured out). I would like the parsing to happen on a background thread, and then update my UI on the main thread. So I added another dispatch_async block inside the success block of the AFXMLRequestOperation:
self.xmlOperation =
[AFXMLRequestOperation XMLParserRequestOperationWithRequest: request
success: ^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
XMLParser.delegate = self;
[XMLParser setShouldProcessNamespaces:YES];
[XMLParser parse];
dispatch_async(dispatch_get_main_queue(), ^{
[self.searchResultViewController didFinishImport];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
});
});
}
failure: ^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
// show error
}];
[self.xmlOperation start];
Is the the proper/correct/preferred way to do this?
This looks pretty good. Two observations, though:
Does any of your code on the main thread can access any of the objects actively being updated by your NSXMLParserDelegate methods? If not, you're fine.
But, if you have any code (driving the UI, for example) that is accessing the same objects/collections that the NSXMLParserDelegate methods are updating, then you have to be careful about synchronizing those shared resources. (For more information about synchronizing resources, see the Synchronization section of the Threading Programming Guide and/or the Eliminating Lock Based Code section of the Concurrency Programming Guide.)
Personally, I like to move the NSXMLParserDelegate code into a separate class, and instantiate that for the individual request, that way I know that my request and subsequent parsing process can never be a source of synchronization issues. You still need to synchronize the update model/store process, but you are effectively doing that by performing that final update on the main queue.
Does your UI allow you to issue another XML request while the first one is in progress? If not, you're fine.
If the user can initiate second request while the first is in progress, it opens you up to the (admittedly unlikely) scenario that you could two concurrent processing requests using the same instance of the delegate object. Clearly, you could solve this by preventing subsequent requests until the first one finished (e.g. disable UI elements that request refresh), or use a serial queue, or move the parser into a separate class that you'll instantiate for every request. Personally, I'd be inclined to make make this parse request cancelable and make the issuance of a new request cancel any prior, on-going ones.
Those are two concurrency-related issues as I look at your code sample. Perhaps neither of these are, in fact, an issue with your particular implementation. Having said that, the very fact that the code is so contingent on the rest of your implementation is, itself, an issue.
I make a queue
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
I send the queue to my async request
[NSURLConnection sendAsynchronousRequest:req queue:operationQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
// ... do stuff here
});
}];
I cancel my operations prematurely
[operationQueue cancelAllOperations];
However, I can see my async's "completion" code still running. How come this scenario doesn't work like I expected?
Isn't it your responsibility to check isCancelled during your operation's task in case it gets canceled halfway though?
NSOperationQueue won't just kill tasks, it will set them as cancelled and let them finish themselves. This lets you clean up any resources you might have allocated and tidy up before you exit.
Tasks that haven't started yet won't start.
cancelAllOperations goes through the items in the queue and calls cancel on each one. If you look at the documentation for completionBlock:
The completion block you provide is executed when the value returned by the isFinished method changes to YES. Thus, this block is executed by the operation object after the operation’s primary task is finished or cancelled.
can be seen here
edit:
another snippet from the documentation for setCompletionBlock:
A finished operation may finish either because it was cancelled or because it successfully completed its task. You should take that fact into account when writing your block code. Similarly, you should not make any assumptions about the successful completion of dependent operations, which may themselves have been cancelled.