iPhone: how to use performSelector:onThread:withObject:waitUntilDone: method?
Asked Answered
C

4

18

I am trying to use a separate thread for working with some API.

The problem is that I am not able to use performSelector:onThread:withObject:waitUntilDone: method with a thread that I' instantiated for this.

My code:

@interface MyObject : NSObject {
  NSThread *_myThread;
}
@property(nonatomic, retain) NSThread *myThread;
@end

@implementation MyObject
@synthesize myThread = _myThread;
- (NSThread *)myThread {
  if (_myThread == nil) {
    NSThread *myThreadTemp = [[NSThread alloc] init];
    [myThreadTemp start];
    self. myThread = myThreadTemp;
    [myThreadTemp release];
  }
  return _myThread;
}

- (id)init {
  if (self = [super init]) {
    [self performSelector:@selector(privateInit:) onThread:[self myThread] withObject:nil waitUntilDone:NO];
  }
  return self;
}
- (void)privateInit:(id)object {
  NSLog(@"MyObject - privateInit start");
}

- (void)dealloc {
  [_myThread release];
  _myThread = nil;
  [super dealloc];
}
@end

"MyObject - privateInit start" is never printed.
What am I missing?

I tried to instantiate the thread with target and selector, tried to wait for method execution completion (waitUntilDone:YES).
Nothing helps.

UPDATE:
I don't need this multithreading for separating costly operations to another thread.
In this case I could use the performSelectorInBackground as mentioned in few answers.
The main reason for this separate thread is the need to perform all the actions in the API (TTS by Loquendo) from one single thread.
Meaning that I have to create an instance of the TTS object and call methods on that object from the same thread all the time.

Cresset answered 6/4, 2010 at 11:26 Comment(0)
C
14

I found an answer!

In order to keep the thread up, there is a need in additional piece of code:

- (void)threadMain:(id)data {
    NSAutoreleasePool *pool = [NSAutoreleasePool new];

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

    while (isAlive) { // 'isAlive' is a variable that is used to control the thread existence...
        [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    [pool release];
}


And the next line:

NSThread *myThreadTemp = [[NSThread alloc] init];

Should be replaced by this one:

NSThread *myThreadTemp = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain:) object:nil];

EDIT: As was suggested by few people here I've added few lines of code (NSAutoreleasePool, addPort method and 'isAlive' boolean).

Cresset answered 10/4, 2010 at 23:49 Comment(15)
Aren't creating an infinite loop that way? If the runloop doesn't have anything to do, it will just quit immediately and all you do is call - run as often as your processor can handle. (CPU utilization at 100 %)Poppo
Infinite - yes. But the loop doesn't iterate all the time. I've put a log (NSLog...) line inside the while and it was called only once...Cresset
This is unexpected behaviour and should not be relied upon. A run loop with no input sources or timers configured should exit immediately, and therefore your loop should be running constantly, over and over again, using 100% CPU, as the poster said. Refer to the nsrunloop docs for more info.Ceporah
Please try the solution before stating such statements and, especially, before reducing someone's reputation. No 100% CPU is used.Cresset
As I said, the documentation clearly states that the NSRunLoop ought to be exiting immediately and 100% cpu ought to be used. Your code is exhibiting unexpected behaviour – either exploiting a bug to function, or working due to some other reason not represented in the code here. A good programmer would be highly concerned by this, not blithely respond with "the code seems to work". The documentation explicitly states contrary behaviour.Ceporah
Though it gives me no joy to reduce your rep, thumbing this answer down was necessary in order to prevent other people seeing this code and thinking it was a good solution. It is not, because 1. it exploits undocumented behaviour and 2. it does not allow for clean termination of the thread. If you would like me to remove the downvote, read the documentation, take people's criticisms on board, and edit your answer with a good solution.Ceporah
Note that threads should also set up their own autorelease pool, as per KermiDT's answer. To make this answer acceptable, 1. add that, 2. add a keepalive system in order to remove the reliance on undocumented behaviour (as per KermiDT: add a selector / kotenok-gav: add a port), 3. change the while loop to check for an exit condition – do { ... } while (!shouldExit) – and 4. add cleanup code (release autoreleasepool and any other variables) after the loop.Ceporah
Here is an xcode project illustrating the expected behaviour. The relevant method is threadMain in MyController.m. When the line adding a dummy port to the run loop is commented out, the thread is pegged at 100%. Why this is not happening in your own code is not clear from the code posted here, but you'd be unwise to proceed too far down the multithreading route without investigating properly.Ceporah
@Benji, I have checked your project and checked my solution once again and came to conclusion that 'addPort' is necessary only in Mac (as it comes out from many runs that I've done). Maybe in iPhone it is done in the background in order to prevent inventors like me from ruining iPhone apps. About the NSAoutoreleasePool: I used to init and release it in each method that is executed in the separate thread - now I understand that it is much easier this way. About the "while (true)": of course I haven't left it like this in my app - just forgot to update the answer. See my updated answer...Cresset
That's pretty interesting about the iPhone projects, I'll take a look. Even the NSRunLoop docs for iPhone (see the "run..." methods) do say the run loop should should exit immediately when no sources or timers are present.Ceporah
OK well I'm getting the expected behaviour (thread uses 100%) with a trivial porting of the above xcode project to iPhone. But the line you've currently commented as having no effect in iPhone definitely is necessary, ordinarily. (Edit: xcode iphone project - monitor top while running it to see the effect.)Ceporah
I have downloaded your project and run it. The runloop runs ONLY when there are actions performed on that thread. I have added one more button and performed a selector on the thread. The runloop returns ONLY when the selector ends each time. See screenshots (dl.dropbox.com/u/2390939/Picture%2011.png and dl.dropbox.com/u/2390939/Picture%2010.png).Cresset
Interesting. Try the same thing, but remove the NSLog calls and instead monitor top – the threads loops infinitely. For some reason putting any NSLog call before (or after, in the loop!) [loop run] will prevent the proper behaviour from being obtained. No explanation for this, except this line from the Foundation Functions Reference seems suggestive: Output from NSLogv is serialized, in that *only one thread in a process can be doing the writing/logging described above at a time*. All attempts at writing/logging a message complete before the next thread can begin its attempts.Ceporah
Ah, here we go – someone who's identified the same interaction, between NSLog and NSRunLoop, and getting a (kind of) answer. NSLog is setting up a hidden port, which is preventing the run loop returning – hence, it is "serialized", though we can't confirm that as the source is not public. Please can you remove the line saying "this only affects mac" – it's not accurate and it worries me that someone is going to see it!Ceporah
Done. I appreciate your research. Do you look for a job? :)Cresset
T
13

This is what works for me. Main loop taken from Apple's documentation http://developer.apple.com/iphone/library/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW25

- (void) start {
    self.imageSaverThread = [[[NSThread alloc] initWithTarget:self selector:@selector(imageSaverThreadMain) object:nil] autorelease];
    [self.imageSaverThread start];
}

- (void) imageSaverKeepAlive {
    [self performSelector:@selector(imageSaverKeepAlive) withObject:nil afterDelay:60];    
}

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

    // Add selector to prevent CFRunLoopRunInMode from returning immediately
    [self performSelector:@selector(imageSaverKeepAlive) withObject:nil afterDelay:60];
    BOOL done = NO;

    do
    {
        NSAutoreleasePool *tempPool = [[NSAutoreleasePool alloc] init];
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;

        [tempPool release];
    }
    while (!done);

    [pool release];
}

Hope it helps

Towrope answered 13/7, 2010 at 9:46 Comment(1)
Now this is a help. Note how some kind of event should be added to prevent the run loop from immediately returning.Ceporah
I
4

Well, I suppose I've got a better solution

- (void)run{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    running = true;
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    while (running && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]){
        //run loop spinned ones
    }

    [pool release];
}

What am i doing here?
1) Adding a mock port here as a Source will prevent runMode:beforeDate: method from exiting immideately.
2) Method runMode:beforeDate: blocks thread until there's something in runLoop.

Iceskate answered 7/5, 2010 at 21:38 Comment(1)
If I call performSelector:onThread: multiple times, do all my selector queue up ?Jann
E
1

You have created the thread, but it is not running. It must run in order to execute something.

You may also use "performSelectorInBackground" instead. It will queue the invocation until the initialization is done.

Electuary answered 6/4, 2010 at 11:36 Comment(5)
Thank you for your comment. [myThreadTemp start]; isn't supposed to make the thread running? performSelectorInBackground is not good for me. I have to use one thread for all the operations on that API I use (TTS by Loquendo).Cresset
In the time between [myThreadTemp start]; and your perform selector, the thread may have stopped.Gristmill
If you need a separate thread, then you can use "detachNewThreadSelector:toTarget:withObject:". It will create and spawn a new thread that will start immediately, so you don't have to care about NSThread creation.Electuary
The problem is that I have to keep the reference to the thread. I use the mentioned API all the time and want to execute ALL the actions on that API from the same thread. If I use the detachNewThreadSelector method then I won't get the reference. I believe that I can get the reference from the executed method but I'm sure that the thread still will be terminated once the method execution will be finished.Cresset
One more thing - if performSelector:onThread:withObject:waitUntilDone: exists then there should be a way to use it. I don't understand how to do it...Cresset

© 2022 - 2024 — McMap. All rights reserved.