dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while(!weakSelf.isAnotherThreadCompleted && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
;
[weakSelf doSomething];
});
Is it correct to put async thread to wait another thread in this way?
You're going to stall the global queue corresponding to DISPATCH_QUEUE_PRIORITY_DEFAULT. If you dispatch something else to this queue, it won't execute until you're done waiting.
In addition, I don't think a run loop will exist in the thread corresponding to this dispatch queue, so you're going to create one by calling - currentRunLoop and since no source will be attached to it, it will return immediately with the value NO (or, there will be a random run loop, and the behavior will be completely unpredictable).
Why don't you just call a block at the end of your working thread? Or use only one mechanism (GCD, NSOperation, threads, run loops) and the synchronization that goes with it, instead of mixing them?
Related
It's simply an experimental code, but I got confused since the code didn't execute as I supposed.
The code is like:
- (void)viewDidLoad {
[super viewDidLoad];
self.myQueue = dispatch_queue_create("com.maxwell.timer", NULL);
dispatch_async(self.myQueue, ^{
self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(#"Hey!");
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
Now, I got a output "Hey!" every 1 second, no problem here. I do know that in a dispatched thread I have to run the runloop explicitly.
The problem came out when I tried to stop the timer.
- (void)stopTimer {
dispatch_async(self.myQueue, ^{
[self.timer invalidate];
self.Timer = nil;
});
}
Actually the code in block wouldn't even execute!
What's more, if I used concurrent queue here (dispatch_asyn(dispatch_get_global_queue(...), ^{...})) it would be all right.
Things I know: each time I dispatch_async, no matter concurrent or serial queue, the code execute in different thread. So strictly I didn't invalidate the timer in the same thread where I added it, but it did invalidate in concurrent thread.
So my question is why it failed to invalidate in serial queue?
The issue is that you have a serial queue on which you call [[NSRunLoop currentRunLoop] run]. But you’re not returning from that call (as long as there are timers and the like on that run loop). As the run documentation says:
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
That has the effect of blocking your serial queue’s thread. Any code dispatched to that queue (such as your attempt to invalidate the timer) won’t run as long as that thread is blocked. You have a “Catch 22".
On top of that, if you’re going to set up a background thread to run a NSTimer, you’ll want to create your own thread for that, not use one of the GCD worker threads. See https://stackoverflow.com/a/38031658/1271826 for an example. But as that answer goes on to describe, the preferred method for running timers on a background thread are dispatch timers, getting you out of the weeds of manipulating threads and run loops.
I guess:
In a serial queue, a task is ready to execute only if its predecessor is finished. Here since a runloop which fires a timer is running, the task of invalidating the timer is waiting (blocked). So the code block is never executed.
I've a serial queue and I use that queue to call a performSelectorWithDelay like below
dispatch_async(serialQueue, ^(void) {
[self performSelector:#selector(fetchConfigFromNetwork) withObject:nil afterDelay:rootConfig.waitTime];
});
However, the method fetchConfigFromNetwork never gets called. However, if instead of serialQueue, I use mainQueue - it starts working.
Cannot understand what's happening here and how to fix it?
The explanation why your code doesn't work is in the documentation: https://developer.apple.com/documentation/objectivec/nsobject/1416176-performselector?language=occ
This method registers with the runloop of its current context, and
depends on that runloop being run on a regular basis to perform
correctly. One common context where you might call this method and end
up registering with a runloop that is not automatically run on a
regular basis is when being invoked by a dispatch queue. If you need
this type of functionality when running on a dispatch queue, you
should use dispatch_after and related methods to get the behavior you
want.
I'm assuming you want that method to be called on the serial queue with a delay. The most straight forward (and recommended way) is to use dispatch_after:
__weak typeof(self) wself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rootConfig.waitTime * NSEC_PER_SEC)), serialQueue, ^{
[wself fetchConfigFromNetwork];
});
This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.
This is the discussion about the method performSelector:withObject:afterDelay:, I think the block of dispatch_async will execute on a new thread (not main thread), but you would not know which thread it is, so you can not new a runloop and open it and assign it to this thread. because the runloop of thread is close in default except the main thread, the timer will wait forever.
On my opinion, you should use NSThread instead of dispatch_async, and create a runloop for the thread that you use, then specified the mode of runloop with NSDefaultRunLoopMode, if you actually want to cancelPreviousPerformRequestsWithTarget, otherwise use dispatch_after instead of performSelector.
That's my understanding. I can't promise it is right.
I recently ran into an issue where deferred selectors weren't firing (an NSTimer and methods called with performSelector:withObject:afterDelay).
I've read Apple's documentation, and it does mention in the special considerations area,
This method registers with the runloop of its current context, and depends on that runloop being run on a regular basis to perform correctly. One common context where you might call this method and end up registering with a runloop that is not automatically run on a regular basis is when being invoked by a dispatch queue. If you need this type of functionality when running on a dispatch queue, you should use dispatch_after and related methods to get the behavior you want.
This makes perfect sense, except for the runloop of its current context part. I found myself confused regarding which runloop it's actually going to. Would it be the thread's main runloop that processes all events, or could it be a different one without our knowledge?
For instance, if I hit a breakpoint before calling performSelector inside a block that is being called as a CoreAnimation completion block, the debugger shows execution is on the main thread. However, calling performSelector:withObject:afterDelay never actually runs the selector. This makes me think that call is effectively registering with the runloop associated with the CoreAnimation framework, so regardless of the performSelector call being executed on the main thread, if the CoreAnimation doesn't poll its runloop, the operation isn't executed.
Replacing this call inside that block with performSelectorOnMainThread:WithObject:waitUntilDone fixes the problem, but I've had a hard time convincing a colleague that this is the root cause.
Update: I was able to trace back the origin of the issue to a UIScrollViewDelegate callback. It makes sense that when a UI delegate callback is invoked that the main runloop would be in UITrackingRunLoopMode. But at that point, the handler will queue a block on a background queue and from there execution will jump across a few other queues, eventually coming back to the main runloop. The catch is that when it comes back to the main runloop, it's still in UITrackingRunLoopMode. I think that the main runloop should have come out of UITracking mode when the delegate method was completed, but when execution gets back to main runloop, it's still in that mode. Deferring the code that kicks off the background queueing of the job from the UIScrollViewDelegate method fixes the problem, e.g [self performSelector:#selector(sendTaskToBackQueue) withObject:nil afterDelay:0 inModes:#[NSDefaultRunLoopMode]]. Is it possible that the runloop mode that is used when the background task is queued back to the main thread is dependent on the mode the runloop was in when it queued the background task?
Essentially, the only change was going from this...
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// Currently in UITrackingRunLoopMode
dispatch_async(someGlobalQueue, someBlock);
// Block execution hops along other queues and eventually comes back to main runloop and will still be in tracking mode.
}
to this
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// Currently in UITrackingRunLoopMode
[self performSelector:#selector(backQueueTask) withObject:nil afterDelay:0 inModes:#[NSDefaultRunLoopMode]];
}
-(void)backQueueTask {
// Currently in NSDefaultRunLoopMode
dispatch_async(someGlobalQueue, someBlock);
// Hops along other queues and eventually comes back to main runloop and will still be in NSDefaultRunLoopMode.
// It's as if the runloop mode when execution returns was dependent on what it was when the background block was queued.
}
There is only one run loop per thread, so if you're on the main thread then you're also on the main run loop. However, a run loop can run in different modes.
There are a few things you can try to get to the bottom of the issue:
You can use +[NSRunLoop currentRunLoop] and +[NSRunLoop mainRunLoop] to verify that you're executing from the main thread and main run loop.
You can also use the current run loop directly with NSTimer to schedule a delayed perform-selector. E.g.:
void (^completionBlock)(BOOL) = ^(BOOL finished) {
NSCAssert([NSRunLoop currentRunLoop] == [NSRunLoop mainRunLoop], #"We're not on the main run loop");
NSRunLoop* runLoop = [NSRunLoop mainRunLoop];
// Immediate invocation.
[runLoop performSelector:#selector(someMethod) target:self argument:nil order:0 modes:#[NSDefaultRunLoopMode]];
// Delayed invocation.
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(someMethod) userInfo:nil repeats:NO];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
};
Those calls are essentially equivalent to -performSelector:withObject: and -performSelector:withObject:afterDelay:.
This allows you to confirm which run loop you're using. If you're on the main run loop and the delayed invocation doesn't run, it's possible that the main run loop is running in a mode that doesn't service timers in the default mode. For example, that can happen when a UIScrollView is tracking touch input.
-performSelector:withObject:afterDelay: doesn't schedule operations on a dispatch queue; it schedules it on the run loop of the current thread. Each thread has one run loop, but somebody has to run the run loop in order for it to execute the actions on it. So it all depends on what thread this code is run on.
If it is run on the main thread, the operation will be scheduled on the main thread's run loop. In event-based applications, UIApplicationMain is called in the main function, which runs a run loop on the main thread for the entire lifetime of the app.
If this is run on another thread that you created, then the operation will be put on that thread's run loop. But unless you explicitly run the thread's run loop, the operations scheduled on the run loop won't run.
If this is run on a GCD dispatch queue, it means it is running on some unknown thread. GCD dispatch queues manage threads internally in a way that is opaque to the user. Generally nobody would have run the run loop on such a thread, so operations scheduled on the run loop won't run. (Of course, you could explicitly run the run loop in the same place that you schedule the operation, but that would block the thread, and thus block the dispatch queue, which wouldn't make that much sense.)
performSelector:withObject:afterDelay this will call the selector on the thread that this function is called.
performSelectorOnMainThread:WithObject:waitUntilDon,this will make sure that the selector is called on main thread
What is run loop:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
I have a couple of questions on NSRunLoops in conjunction with dispatch queues.
In my application's delegate applicaitonDidEnterBackground, I'm creating a UIBackgroundTask.
Following that, I use a timer and run loop to execute a task a few minutes after the app is in the background:
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
[[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
So here are my questions:
Is the above a valid approach or is there a better way to achieve a scheduled task while in the background?
Do I need to stop the run loop once my timer's method has fired? I recall from Apple's documentation that simply calling run on an NSRunLoop is not advisable, but I also understand that in GCD I don't have fine-grain control over which thread is returned.
Thanks for your help.
I have just created a background task with a timer using NSRunLoop and NSTimer in my ViewController:
- (void)runBackgroundTask: (int) time{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSTimer* t = [NSTimer scheduledTimerWithTimeInterval:time target:self selector:#selector(startTrackingBg) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:t forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
To call a function that will verify token validity, etc. Is it possible to end this loop from inside the function? For instance:
-(void)startTrackingBg
{
if(TOKEN IS NOT VALID)
{
STOP_THREAD;
dispatch_sync(dispatch_get_main_queue(), ^{
[self alertStatus:#"Session Lost!" :#"Error!"];
[self popToLogin];
});
}
}
A couple of thoughts:
If you look at the documentation for run, they show a pattern that solves your problem:
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. OS X 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. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
where shouldKeepRunning is set to NO somewhere else in the program.
Having said that, if I were going to start another thread, I wouldn't use up one of the global worker threads, but rather I'd just instantiate my own NSThread.
More critically, depending upon what you're trying to do in this other thread, there are generally much better other patterns than establishing your own run loop.
For example, if I wanted to have timer run something in another queue, I'd use a dispatch timer instead:
#property (nonatomic, strong) dispatch_source_t timer;
and then instantiate and start dispatch timer source to run on your designated GCD queue:
dispatch_queue_t queue = dispatch_queue_create("com.domain.app.polltimer", 0);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), kPollFrequencySeconds * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timer, ^{
<#code to be run upon timer event#>
});
dispatch_resume(self.timer);
Or, if you want to use NSTimer, just schedule that on the main runloop, and have the method it calls dispatch the time consuming task to the background queue at that time. But, either way, I'd avoid adding the overhead of a second run loop.
Having shown you better ways to use timers in background threads, now that you describe the intent (polling a server) I'd actually recommend against using timer at all. Timers are useful when you want some action to be initiated at some regular interval. But in this case, you probably want to initiate the next server request after a certain amount of time after the previous request finished. So, in the completion block of the previous request, you might do something like:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20.0 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
<#code to initiate next request#>
});
Also, I'd personally want to make sure there was a very compelling reason to polling your server. Polling always seems so intuitively appealing and logical, but it is an extravagant use of the user's battery, CPU and data plan. And in those cases where you need the client to respond to server changes quickly, there are often better architectures (sockets, push notifications, etc.).
You do a dispatch async and inside you add a timer to the runloop to run a method periodically? You should add threads to make sure that you use all systems of parallelism at once. ;-)
Seriously: Use dispatch_after() and decide inside the block if you want to do it again.