Search code examples
iosobjective-cgrand-central-dispatchnsrunloopdispatch-after

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


Solution

  • 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.