Dispatchsourcetimer and Swift 3.0

DispatchSourceTimer and Swift 3.0

Make sure the timer doesn't fall out of scope. Unlike Timer (where the RunLoop on which you schedule it keeps the strong reference until the Timer is invalidated), you need to maintain your own strong reference to your GCD timers, e.g.:

private var timer: DispatchSourceTimer?

private func startTimer() {
let queue = DispatchQueue(label: "com.firm.app.timer", attributes: .concurrent)

timer = DispatchSource.makeTimerSource(queue: queue)

timer?.setEventHandler { [weak self] in // `[weak self]` only needed if you reference `self` in this closure and you want to prevent strong reference cycle
print(Date())
}

timer?.schedule(deadline: .now(), repeating: .seconds(5), leeway: .milliseconds(100))

timer?.resume()
}

private func stopTimer() {
timer = nil
}

DispatchSourceTimer on concurrent queue

As you probably know, this libDispatch code is notoriously dense. While it can be edifying to go through it, one should be reluctant to rely upon implementation details, because they are subject to change without warning. One should rely solely upon formal assurances stated in the documentation. Fortunately, setCancelHandler documentation provides such assurances:

The cancellation handler (if specified) is submitted to the source’s target queue in response to a call to a call to the cancel() method once the system has released all references to the source’s underlying handle and the source’s event handler block has returned.

So, in answer to your event/cancelation race, the docs are telling us that the cancelation handler will be called only after the “event handler block has returned.”


This can be verified empirically, too, manifesting the potential race with judicious insertion of sleep calls. Consider this rendition of your second example:

logger.log("starting timer")

let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".timerQueue", attributes: .concurrent)
let timer = DispatchSource.makeTimerSource(queue: queue)

let cancelHandler = DispatchWorkItem {
logger.log("cancel handler")
}

timer.setEventHandler {
logger.log("event handler started")
Thread.sleep(forTimeInterval: 2) // manifest potential race
cancelHandler.cancel()
logger.log("event handler finished")
}

timer.setCancelHandler(handler: cancelHandler)
timer.schedule(wallDeadline: .now() + 1)
timer.activate()

DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
logger.log("canceling timer")
timer.cancel()
}

logger.log("done starting timer")

This produces:

2021-10-05 11:29:45.198865-0700 MyApp[18873:4847424] [ViewController] starting timer
2021-10-05 11:29:45.199588-0700 MyApp[18873:4847424] [ViewController] done starting timer
2021-10-05 11:29:46.199725-0700 MyApp[18873:4847502] [ViewController] event handler started
2021-10-05 11:29:47.387352-0700 MyApp[18873:4847424] [ViewController] canceling timer
2021-10-05 11:29:48.204222-0700 MyApp[18873:4847502] [ViewController] event handler finished

Note, no “cancel handler” message.

So, in short, we can see that GCD resolves this potential race between the event and cancellation handlers, as discussed in the documentation.

Difference between DispatchSourceTimer, Timer and asyncAfter?

Timer is a Swift bridge of NSTimer, which goes back to NeXTSTEP, long, long before Grand Central Dispatch (GCD) and things like DispatchSourceTimer, which didn't come along until 10.6 (in the form of dispatch_source_set_timer) and dispatchAfter (in the form of dispatch_after).

NSTimer is based on the run loop, which was the primary way that concurrency was done until GCD. It's a cooperative concurrency system, designed primary to run on a single thread on a single core (though it can be expanded to multi-threaded environments).

While the run loop is still very important in Cocoa, it is no longer the primary, or even preferred, way to manage concurrency. Since 10.6, GCD has been the increasingly preferred approach (though adding a block-based NSTimer API in the 10.12 timeframe was a welcome modernization).

On the scale of 15 seconds, the efficiency differences are pretty irrelevant. That said, I don't understand your comment "A Timer keeps the CPU from going into the idle state." I don't believe that's true. The CPU will definitely still go into the idle state when waiting on an NSTimer to fire.

I would not set up a run loop just to run an NSTimer. You would be much better off scheduling it on the main runloop and then using DispatchQueue.async to do the actual work on some other queue.

As a broad rule, I use the highest-level tool that meets the need. Those are the ones that Apple is likely to optimize the best over time with me making the fewest changes. For example, NSTimer fire dates are automatically adjusted to improve energy efficiency. With DispatchSourceTimer, you get control over the leeway setting to get the same benefit, but it's up to you to set it (the default is zero, which has the worst energy impact). Of course, the reverse is also true. DispatchSourceTimer is the lowest level and gives you the most control, so if that's what you need, that's the one to use.

For your example, I'd personally probably use a Timer and just dispatch to the private queue as part of the block. But a DispatchSourceTimer would be completely appropriate.

asyncAfter is really a different thing, since it's always a one-shot. That's great if you want a one-shot, but it changes things if you want to repeat. If you just call asyncAfter in the block to repeat, it's going to be 15 seconds after the last time you finished, rather than being spaced 15 seconds apart. The former will tend to drift a bit late over time. The design question is this: if for some reason your task took 5 seconds to complete, would you want the next fire event to happen 15 seconds from the end of that, or would you want a constant 15 seconds between each fire event? Your choice there will determine which tool is correct.

As a slight note there, NSTimer events are always a little later than they are scheduled. GCD events with a leeway setting can be a little early or a little late. As a practical matter, there's no such thing as being "on time" (that's a period of zero length; you're not going to hit it). So the question is always whether you are promised to be late like NSTimer, or you might be early like GCD with leeway.

DispatchSourceTimer how to execute after repeating time

The deadline parameter determines the first delivery time (subject
to timer coalescing, leeway, ...), and the repeating parameter
determines the interval after the first delivery.

Therefore

timer.schedule(deadline: .now() + 30.0, repeating: 30.0, leeway: .seconds(0))

schedules the timer to deliver after 30 seconds, and then again every
30 seconds.



Related Topics



Leave a reply



Submit