PDA

View Full Version : NSTimer or NSThread?




jhaspell
Feb 17, 2008, 08:37 AM
Hi,

I am new to Objective C and Cocoa programming and have a question which I hope can be answered in this forum. I have tried going through the Apple documentation and various Objective C references and have not been able to find the answer.

I have written a very simple program with Xcode 2.5, which recursively generates md5 checksums for all files under a given directory.

I launch the generation via a button on the GUI (generated by Interface Builder and using the standard appController).

I am trying to start my processing and then pass control back to the GUI, to allow cancelling of the current operation and other actions. Initially, when the application was single threaded, there was no way to do this - the main GUI would simply hang, waiting for the processing to finish.

So, initially, I tried creating a new thread to handle the processing, using NSThread detachNewThreadSelector. This seemed to work ok, although I was having huge problems syncing data between the 2 threads to ensure only one additional thread was started at any time. I checked a few forums about the issue and the general consensus was not to use NSThread. It was noted that most new Cocoa programmers incorrectly use NSThread, when an NSTimer would be more suited to the task.

I proceeded to write some NSTimer code to spawn off 0.5 second timer, which works fine while my main routine isn't running (at least it calls my handleTimer method, as I can see the NSLog message every 0.5 seconds). When the main routine is running, I see nothing - it is pretty CPU intensive - could it be simply that the CPU doesn't get a chance to service the timer firing. My main routine isn't class based - it is simpy a C style function with bits of Objective C in it.

I have 3 problems (and one further question):

1. How can I force NSTimer to fire when there is other processing going on. Is there a way to reduce the priority of the other function (like Unix nice?)

2. I update an NStextField in my main processing loop. This never gets updated in the main GUI until the main processing has stopped. I am trying to force updates to the 2 NStextFields an NSProgressIndicator. I thought I could force this by refreshing the main NSWindow. My method is within the main appController class and I can't work out my NSWindow instance name. I can't find a way to do this - is there a way to call refresh on "self"?

3. I still can't interact with my GUI during running of my main processing loop. I guess I need to somehow handle the "event loop" of the GUI in my handleTimer:() method. I have no idea how to do this. This is very similar to question 4 of this thread:
http://forums.macrumors.com/showthread.php?t=168030&highlight=NSTimer
but I can't find an answer there.

4. Should I abandon NSTimer altogether and go back to using threads to do this? If so, then I might try Posix threads, as I'm having no joy with NSThread.

I know I have probably missed something really obvious but I have spent more than a day trying various ways of making this work. I have figured everything else out so far by checking the Apple class references and example code but I really can't figure this one out.

Please help!

Thanks,

Jon

An example of the code I am trying to make work:

@interface md5Controller : NSObject
{
IBOutlet NSTextField *outputTextField1;
IBOutlet NSTextField *outputTextField2;
IBOutlet NSProgressIndicator *progressBar1;
}
- (void) handleTimer: (NSTimer *) timer
- (IBAction)handleCreateMd5:(id)sender;

- (id) init
{
// superclass may return nil
if ( (self = [super init]) )
{
NSLog (@"md5Controller::init()");
}

NSLog (@"md5Controller::init() initialising default preference values");
radioMd5TypeValue = MD5_INTERNAL;
radioFileSystemValue = SYSTEM_UNIX;
switchLogValue = LOG_FALSE;

NSTimer *timer;
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target: self selector: @selector(handleTimer:) userInfo: nil repeats: YES];
return self;
}

- (void) handleTimer: (NSTimer *) timer
{
NSLog (@"md5Controller::handleTimer()");
[progressBar1 displayIfNeeded];
[outputTextField1 setNeedsDisplay:YES];
[outputTextField2 setNeedsDisplay:YES];
}

- (IBAction)handleCreateMd5:(id)sender
{
int result;
NSString *tempString;

NSLog (@"md5Controller::handleCreateMd5()");

tempString = [NSString stringWithString:@"/Users/jon/Music/"];

result = createMd5 (tempString, radioMd5TypeValue, radioFileSystemValue, &progressBar1, &outputTextField1);

}



kainjow
Feb 17, 2008, 08:57 AM
I'd go back to threads, this is what they're for. Timers are for doing a task at certain intervals but if you need timer-like functionality you can do that easily with threads as well.

Make sure your thread method is setup like this:

- (void)threadWorker {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

while (!_threadNotDone) {
// do some work

[outputTextField1 performSelectorOnMainThread ... ];
}

[pool release];
}

First off you need the autorelease pool if you're doing anything with Cocoa. Second if you want to be able to stop/cancel your thread, you don't want to just kill your thread, but you want to gracefully exit, so I used a flag here which you can set outside of the thread (e.g. from a button action) to YES and then the thread loop will check for this value and exit when true. Third, you can use the performSelectorOnMainThread... selector to update a UI element from within a separate thread.

So detach your thread and give it a selector threadWorker and that should get you started on making your app threaded.

If you need timer-based functionality in the thread you can use NSThread's sleepUntilDate: method.

jhaspell
Feb 17, 2008, 10:09 AM
Thank you kainjow. I will return to the NSThread model I tried before NSTimer. This was pretty much working, apart from my inability to access the UI objects declared in the appController.

My worry is how do I get the details of the UI element I want to manipulate to my thread (in this case NSTextField *outputTextField1):
from you example code, you assumed I had access to these objects....
[outputTextField1 performSelectorOnMainThread ... ];

In my non-threaded methods, I pass a reference to the object to the lower level functions and maniplulate it from there.

I previously couldn't work out how to pass an object by reference to the thread via the NSArray containing the other parameters (in my case, NSStrings & NSValues). I can't see any way of referencing the main thread's UI element from another thread.

Sorry if I am missing something really obvious.

Jon

kainjow
Feb 17, 2008, 11:19 AM
Threads can access other objects in memory just like the normal main thread, it's no different. You don't necessarily have to pass in variables into your thread method. Global variables and ivars work normally.

To access UI elements from your thread you do need to make sure any methods you call on them are from the main thread. That is why I used the performSelectorOnMainThread... selector. You need to use this when updating textfields and progress indicators (and all other controls) from your thread. Again, you can access these objects just like you would normally from the main thread (besides explicitly calling their selectors on the main thread).

jhaspell
Feb 17, 2008, 01:46 PM
Thank you kainjow - this has fixed my problem. My TextField and ProgressIndicator are now happily updating during my main processing loop :)

Thank you again for all your help - I really appreciate it.

Jon