Threads |
Modified: |
Downloads
Resources
Apple's Threading OS X and iPhone documentation
Multithreading in Cocoa an overview
Overview
Threads are useful when some operation, such as downloading a file, effectively blocks other execution, such as user interaction.
All applications have a primary or main thread, in which the UI events are managed. Additional threads can be started, that operate in synchronous or asynchronous execution.
Unlike Java, which provides a simple threading mechanism, OS X and iPhone offer many threading options, each appropriate for specific applications. We will examine the main classes.
NSTimer iPhone timer
Timers run in the thread from which they are scheduled. Timers are not a thread but can provide asynchronous execution behavior.
In the following example, a timer decrements a counter each second from 10 to 1 before terminating the timer. The following starts the timer, which runs at one second intervals.
myTimer =
[NSTimer scheduledTimerWithTimeInterval: 1
target:self selector:@selector(countDown)
userInfo:nil
repeats:YES];
countLabel.text = @"Done";Here, the current thread starts the timer and continues execution, the timer firing at one second intervals.
To illustrate, the count label first displays @"Done" from the current thread, the 10 to 1 due to timer firing.
|
#import <UIKit/UIKit.h> @interface iPhoneCountdownViewController : UIViewController { NSTimer *myTimer; int count; IBOutlet UILabel *countLabel; } @end |
|
|
#import "iPhoneCountdownViewController.h"
countLabel.text = @"Done"; |
NSRunLoop
The thread execution at right
illustrates how one thread services multiple events in a run loop.
NSRunLoop class adds and removes service events on an existing thread.
In general, your application does not need to explicitly manage NSRunLoop objects. Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed. It is not possible to create a separate NSRunLoop object; to access the current thread’s run loop, do so with the class method currentRunLoop.
NSTimer objects are added to the current thread run loop or can be added to others by NSRunLoop.
Communication ports which need to service events asynchronously, that is without blocking the current thread, can be added to a run loop. The port is then handled within the thread's run loop without blocking other services in the same thread.
The following schedules inputStream, which reads data from a port, on the currentRunLoop.
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; For a discussion, see NSStream.
For a complete example using NSStream and NSRunLoop, download iPhone Echo Client using NSStream and NSRunLoop.
NSThread
The following example has two threads, the primary thread of all executing applications, and a second child thread created in MyThread class.
When the primary thread terminates, any child threads are automatically terminated. The following blocks the primary thread by requesting user input.
The two threads execute in parallel, each yielding execution to other waiting threads by:
[NSThread sleepForTimeInterval: 0.001]; The second tread is started by:
- NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
- [myThread start];
Line 1:
- creates the NSThread object,
- defines the target object (in this case a MyThread object),
- the method to start (i.e. -run:),
- and the NSObject to pass to -run: method.
Line 2:
- creates the thread
- calls the designated -run: method on that thread,
- returns immediately while the -run: method executes.
The second thread -run: method executes a for statement.
|
#import <Foundation/Foundation.h> @interface MyThread : NSObject {} -(void) startThread; -(void) run: (id) param; -(void) print: (char) ch; @end @implementation MyThread - (void) startThread { NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object: nil]; [myThread start]; // Call -run: on thread } -(void) print: (char) ch { printf("%c", ch); [NSThread sleepForTimeInterval:0.001]; } - (void) run: (id) param { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; int i; for(i=0; i<5; i++) { [ self print: '[' ]; [ self print: 'M' ]; [ self print: 'y' ]; [ self print: ']' ]; [ self print: '\n' ]; } [pool release]; } @end |
int
main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; MyThread *myThread = [[MyThread alloc] init]; [myThread startThread]; int x; for(x=0;x<5;++x) { printf("Main thread x = %i\n",x); [NSThread sleepForTimeInterval: 0.001]; }
|
Note in the output below left, the primary and secondary thread execution is interleaved or parallel.
Each thread puts itself to sleep by:
[NSThread sleepForTimeInterval: 0.001];
allowing other, waiting threads to execute.
Without yielding execution by sleep, a single thread can execute to the exclusion of others, as illustrated at right below.
Output with sleep
|
Output without sleep
|
Output with sleep and synchronization
|
Question - Explain the outputs.
Synchronized Threads
Thread execution can be:
•independent of other threads (i.e. parallel or asynchronous execution)
•dependent (i.e. serialized or synchronous execution) where one thread executes to the exclusion of the other threads
•Thread execution can be controlled in Objective C using the synchronized statement which limits access to a common object to a single thread.
•The synchronization mechanism used is termed a monitor (versus a semaphore, task, or other mechanism)
•A monitor allows only one thread to have access to an object at a time.
•The keyword @synchronized defines a statement where one thread at a time has exclusive access to the object.
@synchronized
In the upper left output, Output with sleep, the primary and secondary thread output is interleaved.
Suppose the -run: method statements required atomic execution, that is all the statements were executed as a unit.
Objective C implements a simple monitor operator,@synchronized, for mutual exclusion on an object, very much as Java.
Note use of retain/release of the common object by both threads, this should always be done for shared objects to ensure that the object is not released by one thread while still in use by another.
|
#import <Foundation/Foundation.h> @interface MyThread : NSObject {} -(void) startThread: (NSString *) common; -(void) run: (NSString *) common; -(void) print: (char) ch; @end @implementation MyThread - (void) startThread: (NSString *) common { NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object: common]; [myThread start]; } -(void) print: (char) ch { printf("%c", ch); [NSThread sleepForTimeInterval:0.001]; } - (void) run: (NSString *) common { [common retain]; int i; for(i=0; i<5; i++)
[common release]; |
int
main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; MyThread *myThread = [[MyThread alloc] init]; NSString *common = @"common object"; [common retain]; [myThread startThread: common]; int x; for(x=0;x<5;++x) {
[NSThread sleepForTimeInterval: 0.001]; |
Output with sleep
|
Output without sleep
|
Output with sleep and synchronization
|
Question - Explain the outputs.
Why threads matter
iPhone unthreaded UI
Threads matter when more than one execution path is possible to be followed simultaneously. A good example is the UI, which should always be responsive.
Executing any blocking operation on the main or primary thread blocks the UI operation.
The following example illustrates that point.
The top progress bar is incremented within the following loop, updating the progress bar value at 1/4 second intervals, within the main thread. During that period, all UI operations, moving the bottom slider and the display of the progress bar, are blocked.
while ( [threadProgressView progress] < 1) {
[NSThread sleepForTimeInterval: 0.25];
[self updateProgressBar];
}
#import "iPhoneUIViewController.h"
@implementation iPhoneUIViewController
@synthesize threadValueLabel, threadProgressView, testValueLabel, threadStartButton;
- (IBAction) startThreadButtonPressed:(UIButton *)sender {
threadStartButton.hidden = YES;
threadValueLabel.text = @"0";
threadProgressView.progress = 0.0;
[self run: nil];
}
- (void) updateProgressBar {
float actual = [threadProgressView progress];
threadValueLabel.text = [NSString stringWithFormat:@"%.2f", actual];
threadProgressView.progress = actual + 0.025;
}
- (void) run: (id) param {
threadStartButton.hidden = NO;
}
- (IBAction) testValueSliderChanged:(UISlider *)sender {
testValueLabel.text = [NSString stringWithFormat:@"%.2f", sender.value];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)dealloc {
[threadValueLabel release];
[threadProgressView release];
[threadStartButton release];
[testValueLabel release];
[super dealloc];
}
@end
|
iPhone UI Update within a Thread iPhone threaded UI
The previous example illustrated one threading issue in an event-driven UI such as the iPhone, that of updating the UI.
The following example updates a progress bar value in a secondary thread.
However, UI updates occur in the main or primary thread.
The following executes -updateProgressBar on the primary thread by:
while ( [threadProgressView progress] < 1) {
[NSThread sleepForTimeInterval: 0.25];
[self
performSelectorOnMainThread: @selector(updateProgressBar)
withObject: nil
waitUntilDone: NO];
}
The effect is to call the -updateProgressBar method on the main or primary thread that handles the UI. The updates to the progress bar are displayed immediately, displaying a progressively moving progress bar.
However,
while ( [threadProgressView progress] < 1) {
[NSThread sleepForTimeInterval: 0.25];
[self updateProgressBar];
}would display the progress bar only after completing the secondary thread. The effect would be to block the UI, only displaying the beginning and ending values.
|
#import "iPhoneUIViewController.h" @implementation iPhoneUIViewController @synthesize threadValueLabel, threadProgressView, testValueLabel, threadStartButton; - (void) startThread { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object: nil]; [myThread start]; [pool release]; } - (IBAction) startThreadButtonPressed:(UIButton *)sender { threadStartButton.hidden = YES; threadValueLabel.text = @"0"; threadProgressView.progress = 0.0; [self startThread]; } - (void) updateProgressBar { float actual = [threadProgressView progress]; threadValueLabel.text = [NSString stringWithFormat:@"%.2f", actual]; threadProgressView.progress = actual + 0.025; } - (void) run: (id) param {
threadStartButton.hidden = NO; |
Thread-safe operations
Thread-safe operations are atomic in the sense that the operation is started and completed by the same thread, without another thread executing the same operation.
Objective C by default generates getter/setter atomic operations for properties:
@property (atomic, retain) (NSString *) x;
@synthesize x;We have seen that @synchronized on a common object enforces atomic access, only one thread can access the common object at a time.
Producer/Consumer Model ProducerConsumer
The producer/consumer model is appropriate where one thread produces some data and another consumes the data.
The example below defines three thread-safe array access operations:
- The producer can always enqueue (add) an element.
- The consumer can only dequeue (remove) when an element exists in the array. Blocks until an element exists.
- The consumer can tryDequeue, dequeue if an element exists in array. Does not block but returns NULL if no element exists or queue locked.
A conditional lock (NSConditionLock) is used to indicate whether the array has elements, 0 for no elements, 1 for elements exist.
For NSConditionLock *theLock, the lock operations are:
- [theLock lock] locks theLock if unlocked, when successful blocks other threads attempting to lock theLock.
- [theLock condition] returns the lock condition (value) in this case NO or YES.
- [theLock unlockWithCondition: YES] unlocks and sets the lock condition to YES.
- [theLock lockWhenCondition: YES] locks when condition is YES and theLock is not locked, otherwise blocks thread until condition is YES.
- [theLock tryLock] returns immediately, YES when lock can be acquired regardless of the condition.
The statement below deserves some discussion:
id element = [[[elements objectAtIndex:0] retain] autorelease]; [[elements objectAtIndex:0] retain] returns and retains the array element.
Adding an element to an NSMutableArray increments the retain counter, deallocating the array decrements the element's retain counter. Without the retain, the element returned could also be deallocated.
The element is retained and returned by dequeue method, autorelease delays the release until later, typically when [pool drain] executes.
|
@interface ThreadSafeQueue : NSObject { NSMutableArray *elements; // NO = no elements // YES = elements NSConditionLock *theLock; } -(id) init; // Enqueues an object -(void) enqueue:(id)object; // Blocks until an object to return -(id) dequeue; // Returns NULL if queue is empty -(id) tryDequeue; -(void) dealloc; @end |
@implementation ThreadSafeQueue -(id) init { if (self = [super init]) { elements = [[NSMutableArray alloc] init]; theLock = [[NSConditionLock alloc] initWithCondition: NO]; } return self; } -(void) enqueue:(id)object { [theLock lock]; [elements addObject: object]; [theLock unlockWithCondition: YES]; } -(id) dequeue { [theLock lockWhenCondition: YES];
[elements removeObjectAtIndex: 0]; |
Example
The following demonstrates ThreadSafeQueue use.
Note:
The secondary thread, MyThread, dequeues five integers.
The primary thread in main enqueues five integers.
Output indicates dequeues are forced to wait until the queue holds a value.
enqueue +0 dequeue -0 enqueue +1 dequeue -1 enqueue +2 dequeue -2 enqueue +3 dequeue -3 enqueue +4 dequeue -4
|
ThreadSafeQueue *q; @interface MyThread : NSObject { NSThread* myThread; } -(void) startThread; -(void) run: (id) param; @end @implementation MyThread - (void) startThread { NSLog(@"startMyThread"); myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object: nil]; [myThread start]; } - (void) run: (id) param { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; int i; for(i=0;i<5;++i) { printf("dequeue %d]\n", [(NSNumber *) [q dequeue] integerValue]); [NSThread sleepForTimeInterval: 0.001]; } [pool release]; } -(void) dealloc { |
int
main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; q = [[ThreadSafeQueue alloc] init]; MyThread *myThread = [[MyThread alloc] init]; [myThread startThread]; int i; for(i=0; i<5; i++) { [q enqueue: [NSNumber numberWithInteger: i]]; [NSThread sleepForTimeInterval: 0.001]; printf("enqueue +%d\n",i); } [q release]; |
Delegates and Threads
iPhone Threaded XML Parser with Delegation
We have used NSURL and other classes that call delegate methods that we define and are called on some predetermined event.
Each defines an -initWithDelegate used to initialize the delegate method.
Importantly, when an event occurs, the delegate executes in the same thread that initialized the delegate method.
That makes sense in most cases, for example, a ViewController delegate method would be called on the main thread, allowing UI updates to occur.
Note that the UI can only be updated in the main thread.
In the following example, an XML file is downloaded from iTunes and parsed in a separate thread. When the parse is completed, a delegate
method in the ViewController is called. See NSXMLParser for details on XML parsing.
The figure at right illustrates the concurrency of the two threads, while the XMLParser object is parsing in one thread, the UI is still active in the Main thread.
There are three main parts to delegation:
- Define the delegate method.
-(void) parseDidComplete {
[self updateTop10TextView];
[xmlParser release];
}
- Initialize the delegate. Note that thread is also initialized to the thread that calls -initWithDelegate
xmlParser = [[XMLParser alloc] initWithDelegate: self];
- (XMLParser *) initWithDelegate: (id) d {
self = [super init];
[self setDelegate: d];
thread = [NSThread currentThread];
return self;
}
- Call the delegate on the delegate's thread.
[delegate performSelector:@selector(parseDidComplete)
onThread: thread
withObject:nil
waitUntilDone:NO];
#import "XMLParser.h" @implementation XMLParser @synthesize delegate, URL;
-(NSString *) getText { return outstring; }
- (void) run: (id) param {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSXMLParser *parser =
[[NSXMLParser alloc] initWithContentsOfURL: [self URL]];
[parser setDelegate: self];
[parser parse];
[parser release];
if ([delegate respondsToSelector:@selector(parseDidComplete)])
[pool release];
}
- (void) parseXML: (NSURL *) url
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
outstring = [[NSMutableString alloc] init];
[self setURL: url];
NSThread* myThread = [[NSThread alloc] initWithTarget: self
selector:@selector(run:)
object: nil];
[myThread start];
[pool release];
}
- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{
if (qName) elementName = qName;
if (elementName)
current = [NSString stringWithString:elementName];
}
- (void)parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
current = nil;
}
- (void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string
{
if (!current) return;
if ([current isEqualToString:@"title"])
[outstring appendFormat:@"%@\n", string];
}
-(void) parserDidEndDocument:(NSXMLParser *)parser {}
-(void) parserDidStartDocument:(NSXMLParser *)parser {}
- (void) dealloc {
[outstring release];
[super dealloc];
}
@end
|
#import "iTunesTop10ViewController.h"
@implementation iTunesTop10ViewController
- (void)viewDidLoad {
[super viewDidLoad];
top10TextView.text = @"";
}
-(IBAction) loadButton: (id) send {
top10TextView.text = @"";
[activityIndicator startAnimating];
NSURL *url =
[NSURL URLWithString:
@"http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStore.woa/wpa/MRSS/topsongs/limit=10/rss.xml"];
}
-(void) updateTop10TextView {
top10TextView.text = [xmlParser getText];
[activityIndicator stopAnimating];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
- (void)viewDidUnload {}
- (void)dealloc {
[super dealloc];
}
@end
|