Cancelling dispatch_after?


Graham Cox
 

Hi all,

Is there a way to cancel a scheduled block used with dispatch_after(…)?

While I’m finding the functionality it offers really useful, it seems like as soon as you use it you have to commit to that code being run, even if circumstances change before it does so. As a result I’m having to use flags and other awkward means to prevent the ‘meat’ of the block running. If not I may have to go back to the -performSelector:afterDelay:.. approach which can be cancelled.

—Graham


Quincey Morris
 

On Nov 8, 2017, at 14:03 , Graham Cox <graham@...> wrote:

Is there a way to cancel a scheduled block used with dispatch_after(…)?

If you mean “cancel” in the sense of “prevent it from starting”, then the answer is yes, you can use “dispatch_block_cancel”:


The original GCD didn’t have this ability, but it was added a few years ago.

You can’t cancel it once it’s started running, except by means of setting a flag as you mentioned earlier.


Graham Cox
 

Makes sense, but there’s a bit of a problem.

To do that I’m going to have to create the block and keep a reference to it. That doesn’t seem to be how the GCD dispatch code snippet works - it just declares the block anonymously as part of the function call. OK, I get how to fix that. But the cancel would have to happen from a different method of the enclosing class, which means that the block reference is going to have to be an ivar (or property). That’s where it gets confusing because it’s unclear how to do that properly. Do I have to copy the block or retain it or what? When it’s just a local variable I don’t have to think about these issues.

In addition, the documentation for dispatch_after says that the function copies and releases the block on behalf of the caller. Wouldn’t that mean that my reference isn’t the same block, so the cancel wouldn’t work anyway?


—Graham

On 9 Nov 2017, at 9:13 am, Quincey Morris <quinceymorris@rivergatesoftware.com> wrote:

On Nov 8, 2017, at 14:03 , Graham Cox <graham@mapdiva.com> wrote:

Is there a way to cancel a scheduled block used with dispatch_after(…)?
If you mean “cancel” in the sense of “prevent it from starting”, then the answer is yes, you can use “dispatch_block_cancel”:

https://developer.apple.com/documentation/dispatch/1431058-dispatch_block_cancel?language=objc

The original GCD didn’t have this ability, but it was added a few years ago.

You can’t cancel it once it’s started running, except by means of setting a flag as you mentioned earlier.


 



On Nov 8, 2017, at 4:15 PM, Graham Cox <graham@...> wrote:

In addition, the documentation for dispatch_after says that the function copies and releases the block on behalf of the caller. Wouldn’t that mean that my reference isn’t the same block, so the cancel wouldn’t work anyway?

I think ARC copies the block automatically if you assign it to a variable instead of just having it as a parameter of a function/method call. (A block really only gets copied once; it’s more like “detached”, so that it can be used after the stack frame that created it has returned. After that, any further copies are no-ops.)

—Jens


Quincey Morris
 

On Nov 8, 2017, at 16:37 , Jens Alfke <jens@...> wrote:

ARC copies the block automatically if you assign it to a variable instead of just having it as a parameter of a function/method call

I think that should be an “or”: if you assign it to a non-local variable *or* pass it as a parameter. That’s because the block is implicitly moved to the heap (which is what a copy does) if the reference to it can escape the current scope:


under the heading “Objects Use Properties to Keep Track of Blocks”.

I think you do have a potential problem, though, if you’re trying to cancel pending invocations when posting a new invocation. If you have an instance property or variable to keep track of the pending block, the logic for cancelling it is not automatically thread safe. (More specifically, the plain logic for testing whether it is still pending is not thread safe.) You’re going to have to figure out a way to make it thread safe. Of course, if you're dispatching these blocks to the main queue, from code executing in the main thread, it’s automatically safe.


 



On Nov 8, 2017, at 5:54 PM, Quincey Morris <quinceymorris@...> wrote:

I think that should be an “or”: if you assign it to a non-local variable *or* pass it as a parameter.

No, because then blocks would always be copied to the heap, even in common cases where they don’t escape, like calling NSDictionary’s -enumerateKeysAndValues: method. That would make these a lot more expensive.

A block literal passed to a function/method is uncopied, just a direct pointer to a structure in the caller’s stack frame. The called function has to copy the block if it will keep a reference that lasts after it returns. This used to be manual, but ARC does it automatically if you assign the block to an ivar or global.

—Jens


Quincey Morris
 

On Nov 9, 2017, at 08:57 , Jens Alfke <jens@...> wrote:

because then blocks would always be copied to the heap, even in common cases where they don’t escape

Yes, that sounds correct. I was thinking that there was a problem for the original caller (the one in whose stack frame the block resides), because the block might have moved after the call returns. But (I presume) this could be handled with one level of indirection in the caller, so that it doesn’t care where the block happens to be, plus the passing of a reference to the pointer, so that the pointer can be updated in the event of a (real) copy.

By contrast, in Swift, block (aka closure) parameters are explicitly annotated as escaping or non-escaping, so this kind of hack is avoidable.


dhoerl
 

The way I always do this:

- create a weakSelf, then in the block assign it to a strongSelf, and test strongSelf for nil
- have a property on your class "delayedBlockCancelled" (you can use an ivar too, then use "->" instead of ".")
- in your dispatch block, if strongSelf is not nil, then see if the property is set or not.

If property is set, then don't do anything.