4
\$\begingroup\$

I created a basic background thread class in swift to replace the ugliness of:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
  // background thread code
  dispatch_async(dispatch_get_main_queue()) { () -> Void in
      // done, back to main thread
  }
}

with the expressiveness of:

Background { () -> () in
    // background code
  }.afterInterval(3) { () -> () in
    // main thread, run after interval IF not done
  }.completion { () -> () in
    // done, main thread
  }

afterInterval and completion are optional and interchangeable.

How reliable is this approach? Can we ever return from init(task) before completion is added, using the dot syntax? I have tested to my abilities and it seems rather bulletproof, but am wondering about the thoughts here.

class Background {

  private var done = false {
    didSet {
      if done {
        taskInterval = nil
        dispatch_async(dispatch_get_main_queue()) { () -> Void in
          completion?()
        }
      }
    }
  }

  private var taskInterval: (() -> ())?
  private var completion: (() -> ())?

  init(task: () -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
      task()
      self.done = true
    }
  }

  func afterInterval(interval: NSTimeInterval, closure: () -> ()) -> Background {
    taskInterval = closure
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC)))
    dispatch_after(delayTime, dispatch_get_main_queue()) { [weak self] in
      self?.taskInterval?()
    }
    return self
  }

  func completion(closure: () -> ()) -> Background {
    completion = closure
    return self
  }

}

"SwiftyThreads" on Github

\$\endgroup\$
1
\$\begingroup\$

Yes, I see what you mean: there do appear to be dodgy race conditions. I do like where you are going with this, though – dot syntax and all. Still I found at least one failing test. If you paste your code in a playground and then add the following:

/* your code here */

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

var i = 0

Background {
    i += 1 // don't call `print` here!
}.afterInterval(0) {
    i += 10
}.completion {
    i += 100
}

let t = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC))
dispatch_after(t, dispatch_get_main_queue()) {
    print(i)
}

// prints: 101 (instead of 111)

True, delay of 0 seems a little contrived, but it does prove that your worries are founded. One way around this is to trigger a dispatch of those closures on demand (and repeatedly):

import Foundation
class Background {
    let qos: qos_class_t
    let task: (() -> ())?
    private(set) var delay: NSTimeInterval? = nil
    private(set) var delayedTask: (() -> ())? = nil
    private(set) var completionHandler: (() -> ())? = nil

    init(_ qos: qos_class_t = QOS_CLASS_BACKGROUND, task: (() -> ())? = nil) {
        self.qos = qos
        self.task = task
    }

    func delay(interval: NSTimeInterval, task: () -> ()) -> Background {
        self.delay = interval
        self.delayedTask = task
        return self
    }

    func completion(task: () -> ()) -> Background {
        self.completionHandler = task
        return self
    }

    func async() {
        dispatch_async(dispatch_get_global_queue(qos, 0)) { // TODO: unnecessary if `task == nil` 
            self.task?()
            if let delayedTask = self.delayedTask, let delay = self.delay {
                let t = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * NSTimeInterval(NSEC_PER_SEC)))
                dispatch_after(t, dispatch_get_main_queue(), delayedTask)
            }
            if let completionHandler = self.completionHandler {
                dispatch_async(dispatch_get_main_queue(), completionHandler)
            }
        }
    }
}

Which can be used like so:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

var i = 0

let bg = Background {
    i += 1
}.delay(0) {
    i += 10
}.completion {
    i += 100
}

// nothing dispatched yet!

bg.async() // repeated invocations possible
bg.async()

Background().delay(1) {
    print(i)
}.async()

// prints: 222 (as it should)

In addition, I would probably rename Background...

\$\endgroup\$
  • \$\begingroup\$ Thanks for the reply, didn't think about testing with a delay of 0. Too focused on the completion block. Anyways, though this is an edge case, it confirms my suspicion that we could get race-y. Thanks again. \$\endgroup\$ – Andrew Robinson Oct 23 '15 at 13:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.