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
}
}
1 Answer 1
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
...
-
\$\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– Andrew Robinson2015年10月23日 13:32:54 +00:00Commented Oct 23, 2015 at 13:32