Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

jll63

macrumors newbie
Original poster
Nov 6, 2010
9
0
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
 
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.
 
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.

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.
 
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.
 
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:
That way your code is inherently sequenced

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
 
Last edited:
I would prefer the controller to be notified

Why wouldn't it be? Send the message from your background thread to the controller.

Don't use performSelectorInBackground:withObject. Use NSOperation.

If I addObject: to an array, isn't the access to the array serialized properly ?
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.
 
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.

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.
 
Why wouldn't it be? Send the message from your background thread to the controller.

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...

Don't use performSelectorInBackground:withObject. Use NSOperation.

Why ?

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.

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

Jean-Louis
 
Last edited:
NSOperation solves your Cancel problem, it will reduce errors. NSOperationQueue also has some other features built in that you get for free.

That adding an object to an array is not atomic is a bit of a shock ;-)
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.
 
NSOperation solves your Cancel problem, it will reduce errors. NSOperationQueue also has some other features built in that you get for free.

Neat stuff there, I'm going to try it.

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.

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
 
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.
 
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.

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
 
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
 
Last edited:
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.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.