views:

1956

answers:

2

I used to think I was a reasonably intelligent person.
apple's "threading programming guide", has shattered my ego sustaining self deception.

I have a method I want to run repeatedly on a secondary thread, in the example below I've called this doStuff:
I want to be able to repeatedly stop and start the repeated calling of this method.

the code starts the thread.
if the boolean value stuffToDo is true,
then it calls doStuff:
otherwise
it has a little rest.
then it loops again until i tell it to stop

My current code seems wasteful, because it keeps checking 'stuffToDo' even when there is nothing to do.

I could get rid of stuffToDo and just spawn and cancel the thread as needed.
This also seems wasteful, and means I would need to be careful not to accidentally spawn a new thread when I already have one running.

I am sure that the answer to efficiently solving my predicament can be found somewhere in the "run loop management" section of Apple's "threading programming guide"
perhaps it involves custom input sources

But I am really finding this document challenging.
It is as if this document is spawning too many threads in my brain and computation grinds to a halt.

enum eRenderThreadMode
{
    render_not_started,
    render_run,
    render_cancel,
    render_finished
};


- (IBAction) startThread:(id)sender
{
    self.renderThreadMode = render_run;
    label.text = @"doing stuff"; 
    [NSThread detachNewThreadSelector:@selector(keepDoingStuff)  toTarget:self withObject:nil];

}

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

    while (renderThreadMode == render_run) 
    {
        if(stuffToDo)
        {
            [self doStuff];
        }
        else
        {
            [NSThread sleepForTimeInterval:kLittleRest];
        }
    }
    self.renderThreadMode = render_finished;
    [pool release];
}


- (IBAction)stopThread:(id)sender
{
    self.renderThreadMode = render_stop;

    while (self.renderThreadMode == render_cancel) 
    {
        [NSThread sleepForTimeInterval:kLittleRest];
    }
}
+5  A: 

You could use a synchronization object that your secondary thread sleeps on. This Apple page indicates there's a facility called Conditions that might do what you want. Using a Condition or similar synchronization object will let your thread only be woken up when there's work to be done (or when it's time for the thread to die).

aem
Agreed. Use an NSConditionLock. This is a standard threading construct.
Jens Alfke
thank you, this looks like it will do exactly what i a want
compound eye
I used NSCondition, as aem suggested, rather than NSConditionLock. Did you actually mean ConditionLock rather that Condition?If so, how would you imagine implementing this using NSConditionLock.I have not worked with objective c threads before, and am interested to hear about different approaches.
compound eye
+4  A: 

Yeah, you are correct that you want to use a runloop, what you are missing is how to set this all up. I am going to modify your post and explain what is going on. Don't worry if it intimidates you, it is tricky and there are some gotchas you only learn about from experience

- (IBAction) startThread:(id)sender
{
    self.renderThreadMode = render_run;
    label.text = @"doing stuff"; 
    self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(keepDoingStuff) object:nil];
    [self.backgroundThread start];    
}

//Okay, this is where we start changing stuff
- (void)keepDoingStuff
{
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        //A runloop with no sources returns immediately from runMode:beforeDate:
        //That will wake up the loop and chew CPU. Add a dummy source to prevent
        //it.

        NSRunLoop *runLopp = [NSRunLoop currentRunLoop];

        NSMachPort *dummyPort = [[NSMachPort alloc] init];
        [runLoop addPort:dummyPort forMode:NSDefaultRunLoopMode];
        [dummyPort release];
        [pool release];

        while (1) 
        {
                NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
                [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                [loopPool drain];
        }
}

Okay, so at this point you should be looking at the above code and thinking "Well, that may be a nice sleeping thread, but it doesn't do anything. And that is true, but since it has an active runloop we can do anything that is runloop based against it, include performSelector:onThread:withObject:waitUntilDone:

- (void) doStuffOnBackgroundThread
{
    [self performSelector:@selector(doStff) onThread:self.backgroundThread withObject:nil waitUntilDone:NO];
}

When you call the above method on your main thread (or any other thread) it will marshall the various arguments and enqueue the runloop of the specified thread, waking it up as necessary. In this case that will cause self.backgroundThread to wakeup from runMode:beforeDate:, execute -doStuff, then cycle back aroound the loop and go back to sleep waiting in runMode:beforeDate:. If you want to be able to tear down the thread you can setup a variable in the while loop like you had in your code, though remember the thread would go away if it is sleep unless you wake it up, so it probably best to encapsulate that in a method that sets the control variable via a performSelector:onThread:withObject:waitUntilDone:, as a plus that will mean the variable will only ever be set from the background thread which simplifies synchronization issues.

Okay, so I think that solves your problem, so time to make the obligatory plug: Are you sure you want to be doing this with threads? NSOperation and NSOperationQueue may be a much simpler solution that takes care of all the threading issues for you if all you need to do is occasionally enqueue some data to have processed. They will schedule work, manage dependencies, and build up/tear down threads as well as taking care of all the runloop wake/sleep stuff.

Louis Gerbarg
thanks for the great detailed answer.In was put off using NSOperation, because I was under the impression NSOperation is designed to process a finite amount of work, where as I want to kick off a thread and keep it running for an indefinite period, (minimum 2 seconds, maximum till the battery runs flat?) so know i am not sure that I need to use threads over NSOperation, it was just based on a vague sense of which felt the most worth investigating
compound eye
You can do either. An NSOperationQueue will keep running until it empty, but you can keep it around and throw more NSOperations into it at any point and have it handle those.
Louis Gerbarg
Thanks Louis,a couple of questions about the above, where does runLoop come from?also in my case when the thread is working, I want it to keep calling doStuff: as fast as it can without my intervention, so i might change the structure so i call something like:[self performSelector:@selector(doAsMuchStuffAsYouCanUntilItellYouToStop) onThread:self.backgroundThread withObject:nil waitUntilDone:NO];
compound eye
The core thing I am confused by it that I don't want to manually keep sending NSOperation objects or keep calling perform selector.<br>My thread (or operation queue) will always have work to do, and I just want to be able to call "start working" and "stop working".<br>When i call start working I want the thread to call doStuff, and when doStuff ends, I want it to call doStuff again, and keep doing so until "stop working" is called.I can imagine lots of gotchas that involve calling a redundant "start" which doesn't get processed until the loop is stopped.
compound eye
Oops, fixed that "NSRunLoop *runLopp = [NSRunLoop currentRunLoop];" Every thread has a runloop that is implicitly created and you get through currentRunLoop being called on that thread. As for calling doStuff as much as you want, if you have another event waiting when the runMode:beforeDate: it will immediately return with that event, so the loop will keep running until all your queued invocations are drained.
Louis Gerbarg
The system can't start and stop methods in mid execution short of completely suspending the thread in kernel. If you want any sort of control over what is going on you need to break it up, which you have done with -doStuff. If you don't want to be manually invoking something from the main thread you can make doStuff enqueue a call to itself before it returns, or make an NSOperation that when finished enqueues another operation.
Louis Gerbarg
This is overkill if you just want to do something simple on a background thread. I prefer the answer below, using an NSConditionLock.
Jens Alfke