macosfoundationcore-foundationnsrunloopcfrunloop

CFRunLoopRun() vs [NSRunLoop run]


I have an NSRunLoop object, to which I attach timers and streams. It works great. Stopping it is another story alltogether.

I run the loop using [runLoop run].

If I try to stop the loop using CRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]), the loop won't stop. If I start the loop using CRunLoopRun() instead, it works. I have also made sure that the call is made on the correct thread (the one running my custom run loop). I have debugged this with pthread_self().

I found a mailing list archive, where a developer said "don't bother using CRunLoopStop() if you started the loop using the run method of NSRunLoop". I can understand why it is the way it is - you normally pair up initializers and finalizers from the same set of functions.

How do you stop an NSRunLoop without "resorting to CF"? I don't see a stop method on NSRunLoop. The docs says that you can stop a run loop in three ways:

  1. Configure the run loop to run with a timeout value
  2. Tell the run loop to stop using CFRunLoopStop()
  3. Remove all input sources, but this is an unreliable way to stop the run loop because you can never know what stuck what into the run loop behind your back

Well, I already tried 2. and there's an "ugly" feel to it, because you have to dig into CF. 3. is out of the question - I don't fancy non deterministic code.

This leaves us with 1. If I understand the docs correctly, you cannot "add" a timeout to an already existing run loop. You can only run new run loops with a timeout. If I run a new run loop, it will not solve my problem, as it will only create a nested run loop. I'll still pop right back into the old one, the same I wanted to stop... right? I might have misunderstood this one. Also, I don't want to run the loop with a timeout value. If I do, I'll have to make a trade off between burning CPU cycles (low timeout value) and responsiveness (high timeout value).

This is the setup I have right now (pseudo code-ish):

Communicator.h

@interface Communicator : NSObject {
    NSThread* commThread;
}

-(void) start;
-(void) stop;
@end

Communicator.m

@interface Communicator (private)
-(void) threadLoop:(id) argument;
-(void) stopThread;
@end

@implementation Communicator
-(void) start {
    thread = [[NSThread alloc] initWithTarget:self 
                                     selector:@selector(threadLoop:)
                                       object:nil];
    [thread start];
}

-(void) stop {
    [self performSelector:@selector(stopThread)
                 onThread:thread
               withObject:self
            waitUntilDone:NO];
    // Code ommitted for waiting for the thread to exit...
    [thread release];
    thread = nil;
}
@end

@implementation Communicator (private)
-(void) stopThread {
    CRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
}

-(void) threadLoop:(id) argument {
    // Code ommitted for setting up auto release pool

    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];

    // Code omitted for adding input sources to the run loop

    CFRunLoopRun();
    // [runLoop run]; <- not stoppable with 

    // Code omitted for draining auto release pools

    // Code omitted for signalling that the thread has exited
}
@endif

What am I to do? Is it common/a good pattern to mess around with CF? I don't know Foundation well enough. Is interfering in the CF layer possibly dangerous (with respect to memory corruption, inconsistencies, memory leaks)? Is there a better pattern to achieve what I am trying to achieve?


Solution

  • You're doing well. There's not problem to use CoreFoundation when you cannot achieve your goal with Foundation. As CoreFoundation is C, it's easier to mess up with memory management but there is no intrinsic danger in using CFRunLoop rather than NSRunLoop (sometimes it may even be safer: CFRunLoop API are thread-safe whereas NSRunLoop isn't).

    If you want to stop your NSRunLoop, you can run it using runMode:beforeDate:. runMode:beforeDate: returns as soon as an input source is processed so you don't need to wait until the timeout date is reached.

     NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
     NSDate *date = [NSDate distantFuture];
     while ( !runLoopIsStopped && [runLoop runMode:NSDefaultRunLoopMode beforeDate:date] );
    

    Then, to stop the run loop, you just need to set runLoopIsStopped to YES.