Prevent dispatch_after() background task from being executed

I answered the question about cancel dispatch_after here. But when i google to find a solution it also return me to this thread, so...

iOS 8 and OS X Yosemite introduced dispatch_block_cancel that allow you to cancel a block before they start executing. You can view detail about that answer here

Using dispatch_after get benefit about using variables that you created in that function and look seamless. If you use NSTimer then you must create a Selector and send variables that you need into userInfo or turn that variables into global variables.


This answer must be posted here: cancel dispatch_after() method?, but that is closed as a duplicate (it really isn't). Anyway, this is a place that google returns for "dispatch_after cancel", so...

This question is pretty fundamental and I'm sure there are people who want a truly generic solution without resorting to various platform-specifics like runloop timers, instance-contained booleans and/or heavy block magic. GCD may be used as a regular C library and there may be no such thing as a timer all in all.

Luckily, there is a way to cancel any dispatch block in any lifetime scheme.

  1. We have to attach a dynamic handle to each block we pass to dispatch_after (or dispatch_async, not really matters).
  2. This handle must exist until the block is actually fired.
  3. Memory management for this handle is not so obvious – if block frees the handle, then we may dereference dangling pointer later, but if we free it, block may do that later.
  4. So, we have to pass ownership on demand.
  5. There are 2 blocks – one is a control block that fires anyway and second is a payload that may be canceled.

struct async_handle {
    char didFire;       // control block did fire
    char shouldCall;    // control block should call payload
    char shouldFree;    // control block is owner of this handle
};

static struct async_handle *
dispatch_after_h(dispatch_time_t when,
                 dispatch_queue_t queue,
                 dispatch_block_t payload)
{
    struct async_handle *handle = malloc(sizeof(*handle));

    handle->didFire = 0;
    handle->shouldCall = 1; // initially, payload should be called
    handle->shouldFree = 0; // and handles belong to owner

    payload = Block_copy(payload);

    dispatch_after(when, queue, ^{
        // this is a control block

        printf("[%p] (control block) call=%d, free=%d\n",
            handle, handle->shouldCall, handle->shouldFree);

        handle->didFire = 1;
        if (handle->shouldCall) payload();
        if (handle->shouldFree) free(handle);
        Block_release(payload);
    });

    return handle; // to owner
}

void
dispatch_cancel_h(struct async_handle *handle)
{
    if (handle->didFire) {
        printf("[%p] (owner) too late, freeing myself\n", handle);
        free(handle);
    }
    else {
        printf("[%p] (owner) set call=0, free=1\n", handle);
        handle->shouldCall = 0;
        handle->shouldFree = 1; // control block is owner now
    }
}

That's it.

The main point is that "owner" should collect handles until it doesn't need them anymore. dispatch_cancel_h() works as a [potentially deferred] destructor for a handle.

C owner example:

size_t n = 100;
struct after_handle *handles[n];

for (size_t i = 0; i < n; i++)
    handles[i] = dispatch_after_h(when, queue, ^{
        printf("working\n");
        sleep(1);
    });

...

// cancel blocks when lifetime is over!

for (size_t i = 0; i < n; i++) {
    dispatch_cancel_h(handles[i]);
    handles[i] = NULL; // not our responsibility now
}

Objective-C ARC example:

- (id)init
{
    self = [super init];
    if (self) {
        queue = dispatch_queue_create("...", DISPATCH_QUEUE_SERIAL);
        handles = [[NSMutableArray alloc] init];
    }
    return self;
}

- (void)submitBlocks
{
    for (int i = 0; i < 100; i++) {
        dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (random() % 10) * NSEC_PER_SEC);

        __unsafe_unretained id this = self; // prevent retain cycles

        struct async_handle *handle = dispatch_after_h(when, queue, ^{
            printf("working (%d)\n", [this someIntValue]);
            sleep(1);
        });
        [handles addObject:[NSValue valueWithPointer:handle]];
    }
}

- (void)cancelAnyBlock
{
    NSUInteger i = random() % [handles count];
    dispatch_cancel_h([handles[i] pointerValue]);
    [handles removeObjectAtIndex:i];
}

- (void)dealloc
{
    for (NSValue *value in handles) {
        struct async_handle *handle = [value pointerValue];
        dispatch_cancel_h(handle);
    }
    // now control blocks will never call payload that
    // dereferences now-dangling self/this.
}

Notes:

  • dispatch_after() originally retains the queue, so it will exist until all control blocks are executed.
  • async_handles are freed if payload is cancelled (or owner's lifetime was over) AND control block was executed.
  • async_handle's dynamic memory overhead is absolutely minor compared to dispatch_after()'s and dispatch_queue_t's internal structures, which retain an actual array of blocks to be submitted and dequeue them when appropriate.
  • You may notice that shouldCall and shouldFree is really the same inverted flag. But your owner instance may pass the ownership and even -[dealloc] itself without actually canceling payload blocks, if these do not depend on "self" or other owner-related data. This could be implemented with additional shouldCallAnyway argument to dispatch_cancel_h().
  • Warning note: this solution also lacks synchronization of didXYZ flags and may cause a race between control block and cancellation routine. Use OSAtomicOr32Barrier() & co to synchronize.

A bit different approach OK, so, with all answers collected, and possible solutions, seems like the best one for this case (preserving simplicity) is calling performSelector:withObject:afterDelay: and cancelling it with cancelPreviousPerformRequestsWithTarget: call when desired. In my case - just before scheduling next delayed call:

[NSObject cancelPreviousPerformRequestsWithTarget: self selector:@selector(myDelayedMethod) object: self];

[self performSelector:@selector(myDelayedMethod) withObject: self afterDelay: desiredDelay];

Why even use GCD? You could just use an NSTimer and invalidate it when your app returns to the foregound.