Andrey Chernukha
Andrey Chernukha

Reputation: 21808

dispatch_after block is not running

Please consider this simple example:

- (void)viewDidLoad
{
    [super viewDidLoad];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"BLOCK!!!");

    });

    while (YES)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        NSLog(@"RUN LOOP");
    }
});
}

The block passed into the second call (3 seconds) to dispatch_after is not fired. However if I don't use the first dispatch_after (2 seconds) then it works as expected. Why?

I know that if I remove the while loop with NSRunLoop running inside then it is working but I need the loop there

Upvotes: 2

Views: 563

Answers (2)

Rob
Rob

Reputation: 438277

You have code which

  • schedules a dispatch_after to run on the main queue; but then
  • blocks the main queue with a while loop that is repeatedly calling the NSRunLoop.

This is just blocking the main thread from doing anything that isn’t invoked directly from the main NSRunLoop.

There are three solutions to this problem:

  1. You can fix this by dispatching the code with the while loop to a global (i.e. background) queue:

    - (void)viewDidLoad{
        [super viewDidLoad];
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
            NSLog(@"OUTER");
    
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"INNER!!!");
            });
    
            while (true) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
            }
        });
    }
    

    This technique is is a permutation of something we used to do in the pre-GCD days. This is rendered largely useless nowadays. It’s just too inefficient.

  2. You can use a NSTimer which is scheduled and run from the NSRunLoop, so, while you’re still blocking the main queue, at least the timer will fire.

    - (void)viewDidLoad{
        [super viewDidLoad];
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"OUTER");
    
            [NSTimer scheduledTimerWithTimeInterval:3 repeats:false block:^(NSTimer * _Nonnull timer) {
                NSLog(@"INNER!!!");
            }];
    
            while (true) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
            }
        });
    }
    

    This not really a solution to the problem (you’re still blocking the main thread from anything not running from the NSRunLoop itself), but is illuminating about the nature of the runloop.

  3. Or, obviously, it’s best to just remove the while loop:

    - (void)viewDidLoad{
        [super viewDidLoad];
    
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"OUTER");
    
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                NSLog(@"INNER!!!");
            });
        });
    }
    

Bottom line, nowadays, you practically never spin on a thread (or its runloop). It’s terribly inefficient and GCD offers far more elegant ways to achieve the desired effect.

Upvotes: 2

Jeffery Thomas
Jeffery Thomas

Reputation: 42598

I don't see any registered input source in the sample. I don't think the dispatch counts as an input source.

In this case, -runMode:beforeDate: would return NO immediately. The infinite loop you have setup would just spin without ever exiting the current run. It never gets back to the top of the run loop and never processes any of the dispatch queues.

In runMode:beforeDate:, the behvior is defined.

Discussion

If no input sources or timers are attached to the run loop, this method exits immediately and returns NO; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. macOS may 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.

Upvotes: 1

Related Questions