Reputation: 21808
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
Reputation: 438277
You have code which
dispatch_after
to run on the main queue; but thenwhile
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:
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.
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.
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
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