Timers vs actors

Is there something like Timer (as per API) that's compatible with actors?

I cooked this one myself:

final class AsyncTimer: @unchecked Sendable {
    var task: Task<Void, Never>!

    func invalidate() {
        task.cancel()
    }

    @discardableResult static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @Sendable @isolated(any) @escaping (AsyncTimer) async -> Void) -> AsyncTimer {
        let timer = AsyncTimer()

        timer.task = Task {
            if repeats {
                while !Task.isCancelled {
                    try? await Task.sleep(for: .seconds(interval))
                    await block(timer)
                }
            } else {
                try? await Task.sleep(for: .seconds(interval))
                if !Task.isCancelled {
                    await block(timer)
                }
            }
        }
        return timer
    }
}

which seems to be working alright (as in a "drop-in" replacement for the Timer API compatible with actors) however if there's something like this already in the standard library I would prefer using that instead.

1 Like

I believe Task.sleep is the concurrency-friendly alternative, as long as you're okay with the lower precision. I also suggest you use something like swift-clocks to give yourself more control while testing.

3 Likes

There's also AsyncTimerSequence in swift-async-algorithms

3 Likes

Also if you implement a timer by using sleep(for:), you’ll end up getting events at a slightly longer interval than expected due to the extra execution time spent between the sleeps.

If keeping up with the frequency matters to you, better remember the original start instant and call Task.sleep(until: start + n * interval) for n = 1, 2, …, like AsyncTimerSequence does.

3 Likes