15

Scenario:

  • User taps a button asking for some kind of modification on address book.
  • A method is called to start this modification and an alert view is shown.
  • In order to show the alert view and keep the UI responsive, I used dispatch_queue:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                     dispatch_sync(dispatch_get_main_queue(), ^{
                       // Show the alert view
                     });
                   });
    
  • Start the process of address book modification using:

    dispatch_async(modifyingAddressBookQueue, ^{});
    

Now, I want to provide the user with the ability to cancel the process anytime (of course before saving the address book). So when he taps the cancel button in the alert sheet, I want to access the dispatch block, set some certain BOOL to stop the process and revert the address book.

The problem is, you can't do that! you can't access the block and change any variable inside it since all variables are copied only once. Any change of variables inside the block while being executed won't be seen by the block.

To sum up: How to stop a going operation using a UI event?

Update:

The code for the process:

- (void) startFixingModification {

    _fixContacts = YES;
    __block BOOL cancelled = NO;

    dispatch_queue_t modifyingAddressBookQueue;
    modifyingAddressBookQueue = dispatch_queue_create(sModifyingAddressBookQueueIdentifier,
                                                      NULL);

    dispatch_async(modifyingAddressBookQueue, ^{

        for (NSMutableDictionary *contactDictionary in _contactArray) {

            if (!cancelled) {
                break;
            }

            i = i + 1;

            BOOL didFixContact = [self fixNumberInContactDictionary:contactDictionary];
            if (!didFixContact) {
                _fixedNumbers = _fixedNumbers - 1;
            }

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [self setAlertViewProgress:i];
                });

            });
        }
    });

    cancelledPtr = &cancelled;

}

Code for alertview (my own lib) delegate

- (void) alertViewProgressCancel:(ASAlertViewProgress *)alertView { // This is a private lib.


    if (cancelledPtr)
    {
        NSLog(@"stopping");

        *cancelledPtr = YES;
    }

}

In interface, I declare

BOOL*   cancelledPtr;

Update 2:

It's getting really frustrating! for the following code

for (NSMutableDictionary *contactDictionary in _contactArray) {

            NSLog(@"%d", _cancelModification);
            if (_cancelModification) {
                break;
            }
}

if _cancelModification is set to YES, the for loop is broken and that's OK. Once I comment out the NSLog line, the _cancelModification is neglected when it changes to YES!

Abdalrahman Shatou
  • 4,550
  • 6
  • 50
  • 79
  • There is no built in support for cancelling a dispatched queue. See http://stackoverflow.com/questions/1550658/dispatch-queues-how-to-tell-if-theyre-running-and-how-to-stop-them – jmstone617 Apr 08 '12 at 22:26
  • Well, this is really weird! So, concurrency is just for acknowledging the user, hey! I am doing something for, just wait! and show a silly activity indicator?! there must be a way to let the user cancel the operation anytime he wants... – Abdalrahman Shatou Apr 08 '12 at 23:09
  • You can check the link I posted above. The problem is this when you kick off an async dispatch queue, there is no safe way to cancel it, because you may end up in an inconsistent state. The WWDC videos on GCD explain this more in depth. – jmstone617 Apr 08 '12 at 23:44
  • Ok, I checked the link with others too before writing my question. Still, if NSOperation is based on GCD and can be cancelled why not for GCD itself? Also, it seems that this problem can be fixed. See "Update 2". However, I need to write down an NSLog line! – Abdalrahman Shatou Apr 08 '12 at 23:53
  • Is cancelModification defined before the for loop? If you're not logging anything, how do you know it's not breaking? – jmstone617 Apr 08 '12 at 23:55
  • I know that because the modification is performed on the address book. It should revert. – Abdalrahman Shatou Apr 09 '12 at 00:06
  • NSOperation is a high level interface on top of GCD. It provides mechanisms for managing dependancies between tasks, canceling tasks, and other things that GCD does not offer out of the box. Some of this functionality works using KVO. That said, you could try to implement the functionality to cancel GCD tasks, but why both when you can use NSOperation instead? – quellish Jun 18 '12 at 07:22

3 Answers3

21

If you declare your BOOL using __block, then it can be changed outside of the block execution, and the block will see the new value. See the documentation for more details.

An example:

@interface SNViewController ()
{
    BOOL*   cancelledPtr;
}

@end

@implementation SNViewController

- (IBAction)start:(id)sender
{
    __block BOOL cancelled = NO;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (!cancelled) {
            NSLog(@"running");
            sleep(1);
        }        
        NSLog(@"stopped");
    });

    cancelledPtr = &cancelled;
}

- (IBAction)stop:(id)sender
{
    if (cancelledPtr)
    {
        NSLog(@"stopping");

        *cancelledPtr = YES;
    }
}

@end

Alternatively, use an ivar in your class to store the BOOL. The block will implicitly make a copy of self and will access the ivar via that. No need for __block.

@interface SNViewController ()
{
    BOOL   cancelled;
}

@end

@implementation SNViewController

- (IBAction)start:(id)sender
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (!cancelled) {
            NSLog(@"running");
            sleep(1);
        }        
        NSLog(@"stopped");
    });
}

- (IBAction)stop:(id)sender
{
    NSLog(@"stopping");
    cancelled = YES;
}

@end
Kurt Revis
  • 27,695
  • 5
  • 68
  • 74
  • I've check that several times and I concluded the following: __block keyword is effective for variable change inside the block. I mean if the variable changed inside the block, the outside scope will be acknowledged of this change. But, not vice versa! if you changed this variable from outside scope, the block will not bother and won't see this change. – Abdalrahman Shatou Apr 08 '12 at 22:25
  • "Variables accessed by the block are copied to the block data structure on the heap so that the block can access them later. When blocks are added to a dispatch queue, these values must typically be left in a read-only format. However, blocks that are executed synchronously can also use variables that have the __block keyword prepended to return data back to the parent’s calling scope." This is from Concurrency Programming Guide. As you see, it returns the data back to the parent's calling scope not vice versa. I even tried that in my code and it didn't work. The block didn't notice any change – Abdalrahman Shatou Apr 08 '12 at 22:29
  • How exactly are you setting the `__block` variable from outside the block? I suspect you're running into the case when the address of the variable changes, after the block is copied. Don't take the address of the variable until after the `dispatch_async()`. – Kurt Revis Apr 08 '12 at 22:34
  • __ block BOOL cancelOperation = NO; then the dispatch block. In the alertview delegate, when the user cancel, I set cancelOperation = YES; However, the block doesn't see this change and continue running – Abdalrahman Shatou Apr 08 '12 at 22:36
  • I added an example of how to set the __block variable from outside the block. I've verified that the block does see the change and does stop when the BOOL is set to YES. – Kurt Revis Apr 08 '12 at 22:41
  • *cancelledPtr = YES; ?? why are you setting the pointer. I didn't do that and that may be the problem. – Abdalrahman Shatou Apr 08 '12 at 22:44
  • That's so the `-stop:` method can access a variable that normally would be out of scope (since the BOOL's scope is only inside `-start:`). I don't quite understand where your declaration of `cancelOperation` is -- where did you put it so that both the starting method, and the alert view delegate method, can see it? – Kurt Revis Apr 08 '12 at 22:48
  • And the even easier way: just store `BOOL cancelled` in your controller object, and have both the block and the alert view delegate method refer to it. No need for `__block` at all. The block implicitly captures the value of `self` and accesses `cancelled` through it. – Kurt Revis Apr 08 '12 at 22:51
  • No, I can't do that. If I simply use a BOOL for this purpose, and I changed it from NO to YES, the block keeps on running neglecting the change! – Abdalrahman Shatou Apr 08 '12 at 23:03
  • Yup! it works for me too "if you use NSLog"!. It's like enforcing the block to read the changed variable. Remove the NSLog and use a for loop or something to check if the for loop is broken when you cancel the operation... – Abdalrahman Shatou Apr 08 '12 at 23:12
  • It still works if you comment out the `NSLog` and the `sleep`. Neither of them are doing any magic. I have `while (!cancelled); NSLog(@"stopped");`, and sure enough, after I press the stop button which sets cancelled to YES, I see "stopped" in the console. – Kurt Revis Apr 08 '12 at 23:23
  • yes, it did work. mmmmmmmmm it may be something in my code. I'll check it again. Thank you for your time. You've been a great help – Abdalrahman Shatou Apr 08 '12 at 23:31
  • Kurt Revis, can you explain how you use the ivar? Can you send the actual code with ivar? – Salih Ozdemir Mar 11 '14 at 02:20
  • @SalihOzdemir it's the second section of code in my answer. – Kurt Revis Mar 11 '14 at 02:29
  • I actually copied the second section of code that you sent. But it's not working. Just keeps saying it's "running". Am I doing wrong with class or something? – Salih Ozdemir Mar 11 '14 at 02:35
  • I can't see what you are doing, exactly, so I can only guess. Did you hook up a button to call the -stop: method? If you did, did you see "stopping" get logged? If it doesn't appear to get called, did you try using the debugger to see what is going on? – Kurt Revis Mar 11 '14 at 02:44
  • Ok, I found the problem. I have called the -stop: method from another view controller and I saw "stopping" get logged. But it didn't say "stopped". Now I tried it within the same view controller and it worked. Now I have a new problem. Can I change the value of BOOL cancelled from another view controller? – Salih Ozdemir Mar 11 '14 at 02:56
  • The value of cancelled changed to YES when I call the -stop: method from another view controller. But it's not changing in dispatch_get_global_queue. – Salih Ozdemir Mar 11 '14 at 03:02
  • You need to start and stop the *same* view controller. It's up to you to figure out how to do that. The view controllers are supposed to be independent objects. – Kurt Revis Mar 11 '14 at 04:20
7

Approach 1

Create a custom dispatch_async method that returns a "cancelable" block.

// The dispatch_cancel_block_t takes as parameter the "cancel" directive to suspend the block execution or not whenever the block to execute is dispatched. 
// The return value is a boolean indicating if the block has already been executed or not.
typedef BOOL (^dispatch_cancel_block_t)(BOOL cancelBlock);

dispatch_cancel_block_t dispatch_async_with_cancel_block(dispatch_queue_t queue, void (^block)())
{
    __block BOOL execute = YES;
    __block BOOL executed = NO;

    dispatch_cancel_block_t cancelBlock = ^BOOL (BOOL cancelled) {
        execute = !cancelled;
        return executed == NO;
    };

    dispatch_async(queue, ^{
        if (execute)
            block();
        executed = YES;
    });

    return cancelBlock;
}

- (void)testCancelableBlock
{
    dispatch_cancel_block_t cancelBlock = dispatch_async_with_cancel_block(dispatch_get_main_queue(), ^{
        NSLog(@"Block 1 executed");
    });

    // Canceling the block execution
    BOOL success1 = cancelBlock(YES);
    NSLog(@"Block is cancelled successfully: %@", success1?@"YES":@"NO");

    // Resuming the block execution
    // BOOL success2 = cancelBlock(NO);
    // NSLog(@"Block is resumed successfully: %@", success2?@"YES":@"NO");
}

Approach 2

Defining a macro for executing a block asynchronously if a condition is validated:

#define dispatch_async_if(queue,condition,block) \
dispatch_async(queue, ^{\
    if (condition == YES)\
        block();\
});

- (void)testConditionBlock
{
    // Creating condition variable
    __block BOOL condition = YES;

    dispatch_async_if(dispatch_get_main_queue(), condition, ^{
        NSLog(@"Block 2 executed");
    });

    // Canceling the block execution
    condition = NO;

    // Also, we could use a method to test the condition status
    dispatch_async_if(dispatch_get_main_queue(), ![self mustCancelBlockExecution], ^{
        NSLog(@"Block 3 executed");
    });
}
vilanovi
  • 2,097
  • 2
  • 21
  • 25
0

Try to apply the following code sample to your situation:

__block UIView * tempView = [[UIView alloc] initWithFrame:CGRectMake(50, 100, 220, 30)];
[tempView setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:tempView];
[tempView release];

__block BOOL cancel = NO;
//点击之后就会开始执行这个方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
    int i = 0;
    while (i < 1000000000 && cancel == NO) {
        i++;
    }
    NSLog(@"Task end: i = %d", i);
    //这个不会执行,因为在之前,gcd task已经结束
    [tempView removeFromSuperview];
});

//1s 之后执行这个方法
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    NSLog(@"A GCD Task Start");
    cancel = YES;
    [tempView setBackgroundColor:[UIColor blackColor]];
});
Lorenzo Dematté
  • 7,638
  • 3
  • 37
  • 77
lingyfh
  • 1,363
  • 18
  • 23