3

I have the following code that checks the RunLoop in an outerloop and then dispatches to the main_thread in an inner loop using dispatch_after. I have two cases where this is called, once when a button is pressed on the nav bar, and the other case is during viewDidAppear. When the code is called in the later, it remains stuck in the RunLoop and my breakpoint never hits my dispatch_after block. Why is dispatch_after getting blocked? I need to do this to update some progress indicators even while the code is executing. Is is obvious why this might work in one case an not the other? I looked at the stack and there is not much else in the stack.

// case 1:

// does not hit breakpoint
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.4 * NSEC_PER_SEC),     dispatch_get_main_queue(), ^
        {
            [ self longRunningOp ];
        });

// case 2:

// This works fine! 
[ self longRunningOp ];



-(void) longRunningOp
{
    bool dispatched = false;
    while (!finished)
    {
        if (dispatched)
        {
            // reusing the same date to avoid buffers building up!
            date = [ date initWithTimeIntervalSinceNow:0 ];
            [ [ NSRunLoop currentRunLoop ] runMode: NSDefaultRunLoopModes beforeDate:date ];
            continue;
        }

        dispatched = true;

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.001 * NSEC_PER_SEC), dispatch_get_main_queue(), ^() {
            // In second case breakpoint never gets here!

            // OPENGL OPS HERE!

            dispatched = false;
        }

    } );
}

I also noticed on other difference.

Success Case: In the case where it works, the UIApplicationMain makes a call out to CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION, this happens when the user presses a nav button.

Failure Case: while in the case where it fails, the UIApplicationMain invokes into CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE, this happens on viewDidAppear.

nishant
  • 736
  • 1
  • 12
  • 22
  • Where do you set `finished` to true to stop the loop in `longRunningOp` ? `longRunningOp` will be blocking the main thread. Long running operations should be dispatched on a background queue. – Paulw11 Mar 19 '16 at 21:22
  • 1
    To keep your sanity, stay away from manipulating run loops. If you ask questions about it on stackoverflow, then you should really stay away from run loops. – gnasher729 Mar 19 '16 at 21:24
  • @Paulw11 - I wish that were the problem, but the problem is that it is not even getting to longRunningOp. So the block fails to get scheduled. This particular long running op is actually processing that I do in OpenGL and needs to be on the main thread. But to answer your question after the long running op is over it will say finished = true; – nishant Mar 19 '16 at 21:27
  • @ gnasher729 - its works for the majority case very well. Only something that I added in a different leg and its not working. – nishant Mar 19 '16 at 21:28
  • I understand that this is just code you have shown to demonstrate your problem, but this solution is horrible. It is effectively polling and that is rarely a good idea. At the very least you should use NSTimer to schedule periodic updates. If the block in case 1 is never reached it means that the main queue is blocked somewhere in code you haven't shown – Paulw11 Mar 19 '16 at 21:31
  • And I agree with @gnasher729, manipulating the main run loop is a terrible idea, you don't know what other tasks may be added to that run loop. I am not clear what younger trying to achieve with that but there is almost certainly a better way – Paulw11 Mar 19 '16 at 21:35
  • What is it that one can do to get the main queue blocked? I am using only dispatch_async, I don't interleave perform selectors. And per my thinking you can have any number of nested dispatch_async's without deadlock issues. I suspect its something to do with the modes on the run loop. About the design - its worked so far, probably not optimal in how its hogging the CPU, I have a long running op that updates a periodic indicator back to the user on progress and so I need to relinquish control just to be sure that the UI is updating. – nishant Mar 19 '16 at 21:38
  • I can't follow your code and obviously we can't see all of your code. Have you shown two alternative methods in your question? `longRunningOp` as shown would certainly block the main queue. And again, don't mess with the runloop. There is almost certainly no reason to do so. There are a number of other synchronisation and task management solutions available – Paulw11 Mar 19 '16 at 21:58
  • I found something that works. The way to think about it is that the long running op is the entire function with the RunLoop. The inner code is long running but the entire code with the while loop is the real long runner. I have updated the description so that it reads more accurate. I take your advice and if trip again I am going to find a way to do it without the run loop. – nishant Mar 19 '16 at 22:04

2 Answers2

4

In your failure case, where you see CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE on the stack, the run loop is “servicing” the main dispatch queue, which means that it is running all the blocks that have been queued on the main queue. One of those blocks ends up sending the viewDidAppear message. So the stack looks something like this:

-[ViewController viewDidAppear:]
-[UIViewController _setViewAppearState:isAnimating:]
-[UIViewController _endAppearanceTransition:]
-[UINavigationController navigationTransitionView:didEndTransition:fromView:toView:]
__49-[UINavigationController _startCustomTransition:]_block_invoke
-[_UIViewControllerTransitionContext completeTransition:]
__53-[_UINavigationParallaxTransition animateTransition:]_block_invoke95
-[UIViewAnimationBlockDelegate _didEndBlockAnimation:finished:context:]
-[UIViewAnimationState sendDelegateAnimationDidStop:finished:]
-[UIViewAnimationState animationDidStop:finished:]
CA::Layer::run_animation_callbacks(void*)
_dispatch_client_callout
_dispatch_main_queue_callback_4CF
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
UIApplicationMain
main
start

Now suppose that in viewDidAppear:, you put another block on the main dispatch queue and then you run the main run loop recursively, so the stack looks like this:

__CFRunLoopRun
CFRunLoopRunSpecific
-[NSRunLoop(NSRunLoop) runMode:beforeDate:]
-[ViewController viewDidAppear:]
-[UIViewController _setViewAppearState:isAnimating:]
-[UIViewController _endAppearanceTransition:]
-[UINavigationController navigationTransitionView:didEndTransition:fromView:toView:]
__49-[UINavigationController _startCustomTransition:]_block_invoke
-[_UIViewControllerTransitionContext completeTransition:]
__53-[_UINavigationParallaxTransition animateTransition:]_block_invoke95
-[UIViewAnimationBlockDelegate _didEndBlockAnimation:finished:context:]
-[UIViewAnimationState sendDelegateAnimationDidStop:finished:]
-[UIViewAnimationState animationDidStop:finished:]
CA::Layer::run_animation_callbacks(void*)
_dispatch_client_callout
_dispatch_main_queue_callback_4CF
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
UIApplicationMain
main
start

Now you seem to think that the run loop should service the main dispatch queue again in this recursive call. But it cannot do that. The main dispatch queue is a serial queue, which means that it never runs more than one block at a time. There is already a main queue block running, called CA::Layer::run_animation_callbacks(void*). No other main queue block can start until that block returns. The run loop knows this, so it doesn't try to service the main queue in this recursive call.

Since the code you posted in your question doesn't actually do any work, it's difficult to give you any specific help. However, you mentioned OpenGL. Apple's OpenGL ES Programming Guide for iOS has a chapter titled “Concurrency and OpenGL ES” that discusses how to move processing to a background thread or queue.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks for a super and great analysis. You are right - that is exactly the stack how it was! Yet something bothers me. When I did dispatch_after , why would it not queue it up on the timer, unwind the stack, and then schedule it. Then there would be no deadlock. It works if I do perform_selector so there is figures it out fine, but not for dispatch_after. – nishant Mar 20 '16 at 15:50
1

After much debugging and thought I have something that works.I can't explain why it does, but it resolves it for me. I can explain why I made the change. I examined the the stack carefully for the success case and saw that it works if the top of stack functions corresponds to a performSelector:withObject:afterDelay call as against a dispatch_async call. This gave me a clue for what I could do. I replaced my outermost dispatch_asynch with performSelector:withObject:afterDelay. It works without a deadlock now. I have listed the failure case and the success case below.

 // This causes the queue to block; -- FAILURE CASE
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.4 * NSEC_PER_SEC),         dispatch_get_main_queue(), ^
    {
        [ self longRunningOp ];
    });


// This keeps the queue running;!!!!! - SUCCESS CASE
[ self performSelector:@selector(longRunningOp) withObject:nil afterDelay:0.4 ];

This is my naive attempt at the reasoning for why this works.

In my RunMode invocation I am using the current loop, so this call depends on how the current loop is setup. Apparently the current loop is setup differently at the top level based on whether one uses performSelector:withObject:afterDelay as against using dispatch_async. It appears further that clicking on navigation bar results in performSelector:withObject:afterDelay invocations and that is a more desirable state to have to the top level stack in than to have the top level be a dispatch async.
I doubt its the best explanation. But its the best I have at the moment.

nishant
  • 736
  • 1
  • 12
  • 22