iosobjective-cmultithreadingnstimerrunloop

Why these code can make a continuous child thread?


- (void)testCreateContinuousChildThread {
    testThread *thread = [[testThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"current thread:%@", [NSThread currentThread]);
        }];
        NSRunLoop *loop = [NSRunLoop currentRunLoop];
        [loop addTimer:timer forMode:NSRunLoopCommonModes];
        [loop run];
    }];
    
    
    [thread start];
    
    [self performSelector:@selector(testSelector) onThread:thread withObject:nil waitUntilDone:YES];
}

- (void)testSelector {
    NSLog(@"testSelector");
}

The class of 'testThread' only contains dealloc method to log message of dealloc. And after running these code, the testThread won't be destroyed automatically.

When I delete this line, the testThread will be destroyed automatically as expected.

[self performSelector:@selector(testSelector) onThread:thread withObject:nil waitUntilDone:YES];

Solution

  • This is a great question that touches on some subtle details of how NSRunLoop works.

    The -run method is not intended to ever return. It may if there are no sources or timers, but this is explicitly not promised (emphasis added):

    Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

    If you want the run loop to terminate, you shouldn't use this method....

    When you call -performSelector:onThread:..., the system will attach a __NSThreadPerformPerform run lop source if one does not already exist (this is an undocumented, internal type). Once attached, this source is never removed, but it is added lazily the first time it's needed.

    Timers are attached to run loops like sources. They are removed when they are invalidated (such as after they fire, if they do not repeat).

    Putting all these facts together explains the behavior.

    When there is no -performSelector:onThread:... call, then after the timer fires there are no sources or timers left on the run loop, and -run exits, terminating the thread, releasing the NSThread object, and calling -dealloc.

    When you do call -performSelector:onThread:..., then there is a __NSThreadPerformPerform source on the thread. It will never be removed, and so -run will never return. The thread will never terminate, and the NSThread object will never be released.

    If you want the thread terminate, you shouldn't use -run. Instead, use -runMode:beforeDate:. For example:

    NSRunLoop *loop = [NSRunLoop currentRunLoop];
    
    // The docs don't warn you to do this. This adds a dummy source so that
    // the loop always has at least one. You can also use a repeating timer.
    [loop addPort:[NSMachPort new] forMode:NSDefaultRunLoopMode];
    
    // Infinite loop until `shouldKeepRunning` is false.
    // `runMode:beforeDate:` will process one source and then return.
    // (A timer is not a "source" for this purpose.)
    while (![self isCancelled] && [loop runMode:NSDefaultRunLoopMode 
                                        beforeDate:[NSDate distantFuture]]);
    

    You can set isCancelled by calling [thread cancel]. Keep in mind that just cancelling won't immediately terminate this thread. It will terminate the next time a source-modifying -performSelector:... is called, after processing the selector.

    If you want to poll shouldKeepRunning instead, you can pass a beforeDate: of the next time you want to poll.

    If you don't want the thread to terminate, then -run is fine. Just add either a repeating timer or a dummy NSMachPort to keep it alive until the first message.

    As a very general rule, NSThread should be avoided in modern ObjC. GCD provides better tools for most problems that were traditionally handled directly NSThread. But threads are still fully supported, so if that matches your needs, it's not wrong.