Objective-C, cancel a dispatch queue using UI event

22,097

Solution 1

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

Solution 2

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");
    });
}
Share:
22,097
Abdalrahman Shatou
Author by

Abdalrahman Shatou

Software Engineer with interest in Mobile, Web, and Backend development and testing.

Updated on November 06, 2020

Comments

  • Abdalrahman Shatou
    Abdalrahman Shatou over 3 years

    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
    Abdalrahman Shatou about 12 years
    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
    Abdalrahman Shatou about 12 years
    "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
  • Kurt Revis
    Kurt Revis about 12 years
    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().
  • Abdalrahman Shatou
    Abdalrahman Shatou about 12 years
    __ 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
  • Kurt Revis
    Kurt Revis about 12 years
    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.
  • Abdalrahman Shatou
    Abdalrahman Shatou about 12 years
    *cancelledPtr = YES; ?? why are you setting the pointer. I didn't do that and that may be the problem.
  • Kurt Revis
    Kurt Revis about 12 years
    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
    Kurt Revis about 12 years
    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.
  • Abdalrahman Shatou
    Abdalrahman Shatou about 12 years
    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
    Abdalrahman Shatou about 12 years
    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...
  • Kurt Revis
    Kurt Revis about 12 years
    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.
  • Abdalrahman Shatou
    Abdalrahman Shatou about 12 years
    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
  • Salih Ozdemir
    Salih Ozdemir about 10 years
    Kurt Revis, can you explain how you use the ivar? Can you send the actual code with ivar?
  • Kurt Revis
    Kurt Revis about 10 years
    @SalihOzdemir it's the second section of code in my answer.
  • Salih Ozdemir
    Salih Ozdemir about 10 years
    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?
  • Kurt Revis
    Kurt Revis about 10 years
    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?
  • Salih Ozdemir
    Salih Ozdemir about 10 years
    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
    Salih Ozdemir about 10 years
    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.
  • Kurt Revis
    Kurt Revis about 10 years
    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.
  • Vyachaslav Gerchicov
    Vyachaslav Gerchicov about 8 years
    could you explain how dispatch_cancel_block_t works?