1
votes

When the expiration handler gets called (synchronously on the main thread) we have to finish up / cancel our task quickly to prevent the app from being terminated.

I read somewhere (but can't find the reference) that all processing must have completely finished by the time the expiration block returns.

Does that mean, if I'm using an NSOperationQueue, that I have to cancel and then wait for the operations to finish before returning? Like this:

backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithName("MyBackgroundTask") {
    operationQueue.cancelAllOperations()
    operationQueue.waitUntilAllOperationsAreFinished() // Wait, so everything finishes before returning from this func (but might deadlock!)
    UIApplication.sharedApplication().endBackgroundTask(backgroundTaskIdentifier)
}

This means locking the main thread, which could cause some tasks that require the background thread to deadlock.

NSOperation's that get cancelled might need a small amount of time (maybe even a second or two) to properly cancel, so how can we ever safely be sure that it's really finished by the time the expiration handler is called, without accidently deadlocking?

1

1 Answers

0
votes

When you call operationQueue.waitUntilAllOperationsAreFinished() on the main thread it will be blocked, and delegates or other code scheduled from other threads onto the main thread will cause a deadlock.

This is one of the rare cases where you have to "busy wait" and poll (thread-safe!) some sort of flag which indicates that a task has been completed. Fortunately, you can leverage a RunLoop without stressing the CPU too much. With a run loop doing the "wait" on the main thread you can also actually execute delegates or continuations scheduled on the main thread without running into a deadlock.

The following code (using Swift) shows how you can use a run loop to accomplish the "busy wait" on the main thread and also a task which executes on the main thread as well:

Suppose you have some "future" which represents the eventual result of an asynchronous task:

public protocol Future : class {        
    var isCompleted: Bool { get }
    func onComplete(f: ()->())        
}

Ideally, you can "register" (one or more) completion handler(s) which get invoked when the future completes.

Now we can define a method runloopWait for the future which "waits" on a thread (which must have a RunLoop) up until the future is completed:

extension Future {

    public func runLoopWait() {
        // The current thread MUST have a run loop and at least one event source!
        // This is difficult to verfy in this method - thus this is simply
        // a prerequisite which must be ensured by the client. If there is no
        // event source, the run lopp may quickly return with the effect that the
        // while loop will "busy wait".

        var context = CFRunLoopSourceContext()
        let runLoop = CFRunLoopGetCurrent()
        let runLoopSource = CFRunLoopSourceCreate(nil, 0, &context)

        CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode)
        self.onComplete() {
            CFRunLoopStop(runLoop);
        }

        while !self.isCompleted {
            CFRunLoopRun()
        }

        CFRunLoopRemoveSource(runLoop, runLoopSource, kCFRunLoopDefaultMode)
    }

}

With this code, the run loop only ever returns when the completion handler has been invoked. That is, you actually don't have to busy wait, rather it's some sort of async wait.

The code below completes the sample with mocking the future and which you can run to examine the solution above. Note, that the implementation of the future is not thread-safe, but should work in this example.

public class Task : Future {

    var _completed = false

    let _task: () -> ()
    var _continuation: ()->()

    public init(task: () -> ()) {
        _task = task
        _continuation = {}
    }

    public var isCompleted: Bool {
        return _completed
    }

    public static func start(t: Task, queue: dispatch_queue_t ) {
        dispatch_async(queue) {
            t._task()
            t._completed = true
            t._continuation()
        }
    }

    public func onComplete(f:()->()) {
        _continuation = f
    }

}



let t = Task() {

    for _ in 0...5 {
        sleep(1)
        print(".", terminator: "")
    }
    print("")
}

// run the task on the main queue:
Task.start(t, queue: dispatch_get_main_queue())

t.runLoopWait() // "wait" on the main queue
print("main thread finished")