Making my app non-blocking

Discussion in 'iOS Programming' started by jll63, Nov 6, 2010.

  1. jll63 macrumors newbie

    Joined:
    Nov 6, 2010
    #1
    Hello,

    I am working on an iPhone app that does some web scraping and podcast loading. I have successfully coded a first version by means of high-level APIs like NSString's stringWithContentsOfURL and now I would like to make my code non-blocking.

    I have read the CFNetwork Programming Guide and I am aware of the techniques described there.

    However, I am wondering if it wouldn't be easier (and robust, and readable etc) to shove my code into a thread. The lengthy/blocking code is located in my controller's viewDidLoad method. Essentially it builds an array of title/URL pairs for the podcasts, to be used in turn for the cells of a TableView.

    The main thread would return either the number of rows or just zero. As the worker thread would grow the array, it would notify the main thread that would tell the view to refresh itself and call the controller. The model in this case is obviously the array.

    If the user gives up and leaves the view, then kill the thread.

    Is this approach sound ? Is it safe to kill the worker thread when it is posting HTTP requests and running regexes with callback blocks ? How do I tell the TableView to refresh itself ? Are there documents at developer.apple.com that I should read ?

    Thanks to all who will feel like helping this rookie ;-)

    Jean-Louis
     
  2. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #2
    Few suggestions on threaded code under Cocoa.

    If you're using NSURLConnection for downloads there's no need to run it on a thread. It's already threaded. Use the async download method and when the data is complete pass it to a background thread for processing, if that processing is or could be long.

    Do not share model objects between threads. Instead produce a data object (NSArray, NSString, NSData, etc.) on the background thread and send it to the main thread using performSelectorOnMainThread. That way your code is inherently sequenced. When the main thread receives the new data it updates its model objects for the table and reloads the table (or rows).

    Do not kill a thread. Cancel it and let the background thread fall off the end.

    Use NSOperation for all this.
     
  3. robbieduncan Moderator emeritus

    robbieduncan

    Joined:
    Jul 24, 2002
    Location:
    London
    #3
    Actually I believe that's incorrect. The async methods of NSURLConnection schedule actions on the NSRunloop. If you're delegate callbacks (in particular the one for when some data is delivered) were to take a lot of time, or worse block, you'd effect the main runloop and all the event processing.
     
  4. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #4
    Yes, of course the callbacks occur on the main thread. But they only take a lot of time if your code takes a lot of time. All of NSURLConnection's work takes place on a background thread.

    The point is to set up the NSURLConnection to do its callbacks on the main thread and then pass the work to be done to a background thread after that. Your callbacks shouldn't be doing much work other than passing the downloaded data to the background thread.

    While it's possible to run NSURLConnection on a background thread it's somewhat complicated, because it depends on the runloop and usually the runloop really isn't running. Also, since NSURLConnection does its work on its own thread you get little or no performance improvement if you run it on a background thread.
     
  5. jll63, Nov 7, 2010
    Last edited: Nov 7, 2010

    jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #5
    Thanks for the input...

    I have implemented my solution. It was very easy thanks to performSelectorInBackground:withObject:. I don't see any reason to get into incremental loading and callbacks, I feel that it would just make my code obscure (I had a look at SeismicXML - whew !) for no profit.

    I still have to deal with the cancellation logic, I got the message, I won't kill the thread.

    I had a little setback wrt refreshing my view, until I found out that reloadData (and all messages sent to a UI object) has to be sent from the main thread. performSelectorOnMainThread:withObject:waitUntilDone: did the trick.

    I'm not very happy with this solution though...I would prefer the controller to be notified (in its main thread) whenever the container changes. I guess I have some more reading to do, I have noticed addObserver: in sample apps, I wonder how easy it is to use (e.g. do I have to add runloops and the like?).

    PhoneyDeveloper:
    I'm not sure I understand your remark. If I addObject: to an array, isn't the access to the array serialized properly ?

    Jean-Louis
     
  6. Luke Redpath macrumors 6502a

    Joined:
    Nov 9, 2007
    Location:
    Colchester, UK
    #6
    You are going about this all wrong. You should not be using threads for networking. Use the appropriate async APIs. It's what they are designed for.
     
  7. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #7
    Why wouldn't it be? Send the message from your background thread to the controller.

    Don't use performSelectorInBackground:withObject. Use NSOperation.

    Of course not. Do what I said upthread. Send the new data to the main thread and then update your data model on the main thread.
     
  8. jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #8
    Well thank you for your advice but before replacing simple code that seems to work with complex code I would like to understand why. That was the intent of my original post.
     
  9. jll63, Nov 7, 2010
    Last edited: Nov 7, 2010

    jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #9
    This question hits the mark. You're talking about swapping messages between threads...I've read part of Apple's documentation about MVC but the Model part seems quite underplayed. It's mVC, sort of. I imagined the View becoming conscious about the Model having changed and calling the Controller to help it refresh itself. Just trying to learn...

    Why ?

    That adding an object to an array is not atomic is a bit of a shock ;-)

    Jean-Louis
     
  10. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #10
    NSOperation solves your Cancel problem, it will reduce errors. NSOperationQueue also has some other features built in that you get for free.

    I feel the earth move under my feet. Threading and Cocoa are a kind of forced marriage. They're in there and they work mostly but they're different from other frameworks.

    Anyway, even if adding an object to an array were atomic you still wouldn't have correct synchronization. If you add an object to an array on the background thread and the user is scrolling the table so the main thread reads from the array it's not guaranteed to get the correct value on the main thread. The only way to avoid that problem is to do what I said: pass the new data to the main thread and add it there and then reload the table.
     
  11. jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #11
    Neat stuff there, I'm going to try it.

    Well I guess I could use @synchronized blocks too. But right now I use performSelectorOnMainThread:withObject:waitUntilDone:, which has room for passing the data. So for the time being I'm proceeding as you say.

    I have a new cause for worry tough. My app uses a navigation controller. When the user enters the Program view for a given program the controller starts a thread in viewDidLoad. The thread gathers extra information (i.e. old podcasts for the program) and sticks it in the Model.

    What if the user leaves the view before the thread exits and my controller gets deallocated ? The thread would send a message to a non-existing object ? To test this scenario I added some logging. To my surprise my app didn't crash and I see in the log that the controller survives until the thread exits. My best hypothesis on this behavior is that performSelectorInBackground implicitly retains the object. Is that correct ?

    Now when I switch to NSOperation-based code, I don't think I'll get this effect. The good approach seems to have the controller register itself as an observer of the model. In fact there's no need to stop the thread if the user leaves the view, as the gathered extra data may be useful if he re-selects the same program.

    One more concern: I have put a trace in the viewDidUnload method and it seems that it's not called when the user leaves the view. What's up ?

    Jean-Louis
     
  12. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #12
    Yes, the cancelling-and-sending-a-message-to-a-dealloced-delegate problem is real. The solution is for your NSOperation subclass to do its messaging like this


    Code:
    // main thread
    - (void)reportProgress:(NSDictionary*)data
    {
    	if (! [self isCancelled] && mDelegate && [mDelegate respondsToSelector:@selector(reportProgress:)])
    	{	
    		[mDelegate reportProgress:data];
    	}
    }
    
    // background thread
    - (void)reportProgressInternal:(NSDictionary*) data
    {
    	[self performSelectorOnMainThread:@selector(reportProgress:) data waitUntilDone:NO];
    }
    
    Your background thread code that does the work calls reportProgressInternal: Your view controller cancels the operation in its dealloc method or elsewhere.

    All of the sequencing happens implicitly, and correctly, and there are no locks.

    BTW, you misunderstand the purpose of viewDidUnload. Look it up again.
     
  13. jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #13
    What if the background task is preempted between the if() test and its body ? And the main thread runs in between and the controller gets deallocated ? Sure it cancels, but when the background thread resumes it runs right into reportProgress :eek:

    Another thing, I launch my worker task via dispatch_async, what object do I send isCancelled to ? I tried self hoping it would be an underlying NSBlockOperation but it's the controller's self.

    J-L
     
  14. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #14
    reportProgress: runs on the main thread. It can't be pre-empted by the main thread.
     
  15. jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #15
    Right, I didn't read your code properly...

    J-L
     
  16. jll63, Nov 8, 2010
    Last edited: Nov 8, 2010

    jll63 thread starter macrumors newbie

    Joined:
    Nov 6, 2010
    #16
    This seems neat:

    Code:
    - (void)viewDidLoad {
        [super viewDidLoad];
    	
        [self.program addObserver:self forKeyPath:@"podcasts" options:0 context:NULL];
    	
        if (!self.program.hasOldPodcasts) {
    	self.program.hasOldPodcasts = TRUE;
    	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        	    // collect older podcasts
    	    self.program.podcasts = podcasts; // new, bigger array
    	});
        }
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath
     		  ofObject:(id)object
                        change:(NSDictionary *)change
                        context:(void *)context {
    	NSLog(@"Program has changed");
    	[self performSelectorOnMainThread:@selector(refresh) withObject:nil waitUntilDone:NO];
    	
    }
    
    - (void)refresh {
    	[[self view] reloadData];
    }
    
    - (void)dealloc {
    	[self.program removeObserver:self forKeyPath:@"podcasts"];
            [super dealloc];
    }
    
    So podcasts is an array of Podcast objects for a given Program. I know that I'm not serializing access to the podcasts property but I don't care as long as it's atomic. If I'm an observer of the Program I'm alive. If I feel myself going away, I unsubscribe from the notifications. What can go wrong ?

    Hmm, could the controller be deallocated (because of user activity) between the moment when performSelectorOnMainThread is executed and the message actually makes it to the main thread ?

    J-L
     
  17. PhoneyDeveloper macrumors 68030

    PhoneyDeveloper

    Joined:
    Sep 2, 2008
    #17
    performSelectorOnMainThread retains its target object so usually the object can't go away. However, what if the view controller is already in [super dealloc] or is in the middle of its dealloc method when the performSelectorOnMainThread is queued?

    I've dealt with a problem like this using CATiledLayer, which draws on a background thread. The view that this layer is in can be dealloced while drawing is happening on the background thread with bad results.

    What I do is use @synchronized in dealloc and the background thread. I set a BOOL inDealloc ivar in the dealloc method. On the background thread I check the ivar and don't access the object's other ivars, which have been dealloced by now, if the main thread is inDealloc. In your case don't post the perform if inDealloc.

    This should probably work in your case.
     

Share This Page