Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asynchronously dispatched recursive blocks

Suppose I run this code:

__block int step = 0;

__block dispatch_block_t myBlock;

myBlock = ^{
     if(step == STEPS_COUNT)
     {
         return;
     }

     step++;
     dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2);
     dispatch_after(delay, dispatch_get_current_queue(), myBlock);
};

dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2);
dispatch_after(delay, dispatch_get_current_queue(), myBlock);

The block is invoked once from outside. When the inner invocation is reached, the program crashes without any details. If I use direct invocations everywhere instead of GCD dispatches, everything works fine.

I've also tried calling dispatch_after with a copy of the block. I don't know if this was a step in the right direction or not, but it wasn't enough to make it work.

Ideas?

like image 737
Mihai Damian Avatar asked Mar 22 '11 16:03

Mihai Damian


3 Answers

When trying to solve this problem, I found a snippet of code that solves much of the recursive block related issues. I have not been able to find the source again, but still have the code:

// in some imported file
dispatch_block_t RecursiveBlock(void (^block)(dispatch_block_t recurse)) {
    return ^{ block(RecursiveBlock(block)); };
}

// in your method
dispatch_block_t completeTaskWhenSempahoreOpen = RecursiveBlock(^(dispatch_block_t recurse) {
    if ([self isSemaphoreOpen]) {
        [self completeTask];
    } else {
        double delayInSeconds = 0.3;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), recurse);
    }
});

completeTaskWhenSempahoreOpen();

RecursiveBlock allows for non-argument blocks. It can be rewritten for single or multiple argument blocks. The memory management is simplified using this construct, there is no chance of a retain cycle for example.

like image 77
Berik Avatar answered Oct 16 '22 08:10

Berik


My solution was derived entirely from Berik's, so he gets all the credit here. I just felt that a more general framework was needed for the "recursive blocks" problem space (that I haven't found elsewhere), including for the asynchronous case, which is covered here.

Using these three first definitions makes the fourth and fifth methods - which are simply examples - possible, which is an incredibly easy, foolproof, and (I believe) memory-safe way to recurse any block to arbitrary limits.

dispatch_block_t RecursiveBlock(void (^block)(dispatch_block_t recurse)) {
    return ^() {
        block(RecursiveBlock(block));
    };
}

void recurse(void(^recursable)(BOOL *stop))
{
    // in your method
    __block BOOL stop = NO;
    RecursiveBlock(^(dispatch_block_t recurse) {
        if ( !stop ) {
            //Work
            recursable(&stop);

            //Repeat
            recurse();
        }
    })();
}

void recurseAfter(void(^recursable)(BOOL *stop, double *delay))
{
    // in your method
    __block BOOL stop = NO;
    __block double delay = 0;
    RecursiveBlock(^(dispatch_block_t recurse) {
        if ( !stop ) {
            //Work
            recursable(&stop, &delay);

            //Repeat
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), recurse);
        }
    })();
}

You'll note that in the following two examples that the machinery of interacting with the recursion mechanism is extremely lightweight, basically amounting to having to wrap a block in recurse and that block must take a BOOL *stop variable, which should be set at some point to exit recursion (a familiar pattern in some of the Cocoa block iterators).

- (void)recurseTo:(int)max
{
    __block int i = 0;
    void (^recursable)(BOOL *) = ^(BOOL *stop) {
        //Do
        NSLog(@"testing: %d", i);

        //Criteria
        i++;
        if ( i >= max ) {
            *stop = YES;
        }
    };

    recurse(recursable);
}

+ (void)makeSizeGoldenRatio:(UIView *)view
{
    __block CGFloat fibonacci_1_h = 1.f;
    __block CGFloat fibonacci_2_w = 1.f;
    recurse(^(BOOL *stop) {
        //Criteria
        if ( fibonacci_2_w > view.superview.bounds.size.width || fibonacci_1_h > view.superview.bounds.size.height ) {
            //Calculate
            CGFloat goldenRatio = fibonacci_2_w/fibonacci_1_h;

            //Frame
            CGRect newFrame = view.frame;
            newFrame.size.width = fibonacci_1_h;
            newFrame.size.height = goldenRatio*newFrame.size.width;
            view.frame = newFrame;

            //Done
            *stop = YES;

            NSLog(@"Golden Ratio %f -> %@ for view", goldenRatio, NSStringFromCGRect(view.frame));
        } else {
            //Iterate
            CGFloat old_fibonnaci_2 = fibonacci_2_w;
            fibonacci_2_w = fibonacci_2_w + fibonacci_1_h;
            fibonacci_1_h = old_fibonnaci_2;

            NSLog(@"Fibonnaci: %f %f", fibonacci_1_h, fibonacci_2_w);
        }
    });
}

recurseAfter works much the same, though I won't offer a contrived example here. I am using all three of these without issue, replacing my old -performBlock:afterDelay: pattern.

like image 5
SG1 Avatar answered Oct 16 '22 10:10

SG1


It looks like there are no problem except delay variable. The block uses always the same time that is generated at line 1. You have to call dispatch_time every time if you want to delay dispatching the block.

    step++;
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2);
    dispatch_after(delay, dispatch_get_current_queue(), myBlock);
};

EDIT:

I understand.

The block is stored in stack by the block literal. myBlock variable is substituted for the address of the block in stack.

First dispatch_after copied the block from myBlock variable that is the address in stack. And this address is valid at this time. The block is in the current scope.

After that, the block is scoped out. myBlock variable has invalid address at this time. dispatch_after has the copied block in heap. It is safe.

And then, second dispatch_after in the block tries to copy from myBlock variable that is invalid address because the block in stack was already scoped out. It will execute corrupted block in stack.

Thus, you have to Block_copy the block.

myBlock = Block_copy(^{
    ...
});

And don't forget Block_release the block when you don't need it any more.

Block_release(myBlock);
like image 4
Kazuki Sakamoto Avatar answered Oct 16 '22 09:10

Kazuki Sakamoto