Objective-C, cancel a dispatch queue using UI event
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");
});
}
Abdalrahman Shatou
Software Engineer with interest in Mobile, Web, and Backend development and testing.
Updated on November 06, 2020Comments
-
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 about 12 yearsI'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 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 about 12 yearsHow 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 thedispatch_async()
. -
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 about 12 yearsI 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 about 12 years*cancelledPtr = YES; ?? why are you setting the pointer. I didn't do that and that may be the problem.
-
Kurt Revis about 12 yearsThat'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 ofcancelOperation
is -- where did you put it so that both the starting method, and the alert view delegate method, can see it? -
Kurt Revis about 12 yearsAnd 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 ofself
and accessescancelled
through it. -
Abdalrahman Shatou about 12 yearsNo, 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 about 12 yearsYup! 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 about 12 yearsIt still works if you comment out the
NSLog
and thesleep
. Neither of them are doing any magic. I havewhile (!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 about 12 yearsyes, 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 about 10 yearsKurt Revis, can you explain how you use the ivar? Can you send the actual code with ivar?
-
Kurt Revis about 10 years@SalihOzdemir it's the second section of code in my answer.
-
Salih Ozdemir about 10 yearsI 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 about 10 yearsI 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 about 10 yearsOk, 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 about 10 yearsThe 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 about 10 yearsYou 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 about 8 yearscould you explain how
dispatch_cancel_block_t
works?