🔁

Swift Concurrency 時代の「n秒ごとに処理を実行する」

2024/04/29に公開

Timer とか使うんだっけ…」「while ループで無限ループを作ってn秒待機させてから実行…か……?」よりも Swift Concurrency っぽいコードを目指します。

まとめ

すべて「約1秒ごとに処理を実行し始める」コード

AsyncTimerSequence(Swift 5.7+、Swift Async Algorithms)
import AsyncAlgorithms
import Foundation

let timer = AsyncTimerSequence(
    interval: .seconds(1),
    clock: .continuous
)

let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        for await _ in timer {
            group.addTask {
                // 処理(async なメソッド等も呼び出し可能)
            }
        }
    }
}

// タイマーを止めたいときは
timerTask.cancel()
AsyncStream(unfolding:onCancel:) と Task.sleep(for:tolerance:clock:) の組み合わせ(Swift 5.7+)
import Foundation

let timer = AsyncStream {
    try? await Task.sleep(for: .seconds(1))
}

let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        for await _ in timer {
            group.addTask {
                // 処理(async なメソッド等も呼び出し可能)
            }
        }
    }
}

// タイマーを止めたいときは
timerTask.cancel()
AsyncStream(unfolding:onCancel:) と Task.sleep(nanoseconds:) の組み合わせ(Swift 5.5+)
import Foundation

let timer = AsyncStream {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
}

let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        for await _ in timer {
            group.addTask {
                // 処理(async なメソッド等も呼び出し可能)
            }
        }
    }
}

// タイマーを止めたいときは
timerTask.cancel()
Timer.TimerPublisher(Swift 5.5+、Combine)
import Combine
import Foundation

let timer = Timer
    .publish(every: 1, on: .current, in: .common) // ⚠️ Class property 'current' is unavailable from asynchronous contexts; currentRunLoop cannot be used from async contexts.; this is an error in Swift 6
    .autoconnect()

let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        for await _ in timer.values {
            group.addTask {
                // 処理(async なメソッド等も呼び出し可能)
            }
        }
    }
}

// タイマーを止めたいときは
timerTask.cancel()
async を考慮しない Timer.TimerPublisher(Combine)
import Combine
import Foundation

let timer = Timer
    .publish(every: 1, on: .current, in: .common)
    .autoconnect()

let cancellable = timer.sink { _ in
    // 処理(async なメソッド等は呼び出し不可)
}

// タイマーを止めたいときは
cancellable.cancel()

AsyncTimerSequence を使う

Swift のバージョン 対応環境
Swift 5.7+ iOS 16.0+, macOS 13.0+, tvOS 16.0+, watchOS 9.0+, visionOS 1.0+, Linux, Windows

個人的にもっともおすすめしたいのは Swift Async Algorithms に含まれる AsyncTimerSequence を使う方法です。

AsyncTimerSequenceAsyncSequence に適合しているため、for-await-in ループによって「n秒ごとにループの中が呼ばれる」を実現できます。

currentTime の実装
import Foundation

/// 現在時刻(0時12分34秒なら `"0:12:34"`)
var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
import AsyncAlgorithms
import Foundation

let timer = AsyncTimerSequence(
    interval: .seconds(1),
    clock: .continuous
)

let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        print("FIRED", currentTime)
    }
}

/*
 BEGIN 0:00:00
 FIRED 0:00:01
 FIRED 0:00:02
 FIRED 0:00:03
 FIRED 0:00:04
 ... と約1秒ごとに出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

この方法でうれしいのは、タイマー処理が書かれている for-await-in ループはすでに Concurrency をサポートしている環境下にあるので、そのまま async なメソッド等を呼び出すことが可能なことです。

ここで、処理に約2秒を要する someFunc(num:) を用意します。

処理に約2秒かかる someFunc(num:) の実装
import Foundation

/// 処理に約2秒かかる非同期処理
func someFunc(num: Int) async {
    /// 現在時刻(0時12分34秒なら `"0:12:34"`)
    var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
    
    print(num, "START", currentTime)
    do {
        try await Task.sleep(for: .seconds(2))
        print(num, "FINISH", currentTime)
    } catch {
        print(num, "CANCELLED", currentTime)
    }
}

最低n秒の間隔を空けて処理を繰り返し実行する

AsyncTimerSequence の間隔を約1秒に設定し、以下のようなコード例の挙動を見てみましょう。

import AsyncAlgorithms
import Foundation

let timer = AsyncTimerSequence(
    interval: .seconds(1),
    clock: .continuous
)

var num = 0
let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        num += 1
        print(num, "FIRED", currentTime)
        await someFunc(num: num)
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 1 FINISH 0:00:03
 2 FIRED 0:00:03
 2 START 0:00:03
 2 FINISH 0:00:05
 3 FIRED 0:00:05
 3 START 0:00:05
 3 FINISH 0:00:07
 4 FIRED 0:00:07
 4 START 0:00:07
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

実際の出力を見ると約2秒ごと "FIRED" が出力されています。

これは、someFunc(num:) をその場で await しているため、ループの中の処理はそこで suspend された状態になるのが原因です。someFunc(num:) の処理が終了してループの処理が1つ終わった後、指定時間(今回は約1秒)を経過済みであれば即座に次のループが始まります。

n秒ごとに処理を繰り返して実行し始める

次に、someFunc(num:) にかかる処理時間に依らず、指定時間ごとにループ中の処理が開始されるようにコードを見直します。

import AsyncAlgorithms
import Foundation

let timer = AsyncTimerSequence(
    interval: .seconds(1),
    clock: .continuous
)

var num = 0
let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        print("BEGIN", currentTime)
        for await _ in timer {
            num += 1
            print(num, "FIRED", currentTime)
            group.addTask {
                await someFunc(num: num)
            }
        }
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 2 FIRED 0:00:02
 2 START 0:00:02
 3 FIRED 0:00:03
 3 START 0:00:03
 1 FINISH 0:00:03
 4 FIRED 0:00:04
 4 START 0:00:04
 2 FINISH 0:00:04
 5 FIRED 0:00:05
 5 START 0:00:05
 3 FINISH 0:00:05
 6 FIRED 0:00:06
 6 START 0:00:06
 4 FINISH 0:00:06
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

for-await-in ループを withTaskGroup(of:returning:body:) で包んであげるようにしました。こうすることで、"FIRED" が約1秒ごとに出力されているのが見え、someFunc(num:) はそれぞれにかかる処理時間に依らずに並行に処理されているようすが見えます。

AsyncStream(unfolding:onCancel:)Task.sleep(for:tolerance:clock:) を組み合わせて使う

Swift のバージョン 対応環境
Swift 5.7+ iOS 16.0+, macOS 13.0+, tvOS 16.0+, watchOS 9.0+, visionOS 1.0+, Linux (Swift 5.7.1+), Windows (Swift 5.7.1+)

AsyncStream(unfolding:onCancel:) と Swift 5.7 より利用可能になった Task.sleep(for:tolerance:clock:) を組み合わせて使う方法です。

AsyncStreamAsyncSequence に適合しているため、for-await-in ループによって「n秒ごとにループの中が呼ばれる」を実現できます。

currentTime の実装
import Foundation

/// 現在時刻(0時12分34秒なら `"0:12:34"`)
var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
import Foundation

let timer = AsyncStream {
    try? await Task.sleep(for: .seconds(1))
}

let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        print("FIRED", currentTime)
    }
}

/*
 BEGIN 0:00:00
 FIRED 0:00:01
 FIRED 0:00:02
 FIRED 0:00:03
 FIRED 0:00:04
 ... と約1秒ごとに出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

この方法でうれしいのは、タイマー処理が書かれている for-await-in ループはすでに Concurrency をサポートしている環境下にあるので、そのまま async なメソッド等を呼び出すことが可能なことです。

ここで、処理に約2秒を要する someFunc(num:) を用意します。

処理に約2秒かかる someFunc(num:) の実装
import Foundation

/// 処理に約2秒かかる非同期処理
func someFunc(num: Int) async {
    /// 現在時刻(0時12分34秒なら `"0:12:34"`)
    var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
    
    print(num, "START", currentTime)
    do {
        try await Task.sleep(for: .seconds(2))
        print(num, "FINISH", currentTime)
    } catch {
        print(num, "CANCELLED", currentTime)
    }
}

処理と処理の間をn秒空けて繰り返し実行する

AsyncStreamproduce の間隔を、Task.sleep(for:tolerance:clock:) を用いて約1秒に設定し、以下のようなコード例の挙動を見てみましょう。

import Foundation

let timer = AsyncStream {
    try? await Task.sleep(for: .seconds(1))
}

var num = 0
let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        num += 1
        print(num, "FIRED", currentTime)
        await someFunc(num: num)
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 1 FINISH 0:00:03
 2 FIRED 0:00:04
 2 START 0:00:04
 2 FINISH 0:00:06
 3 FIRED 0:00:07
 3 START 0:00:07
 3 FINISH 0:00:09
 4 FIRED 0:00:10
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

実際の出力を見ると約3秒ごとに "FIRED" が出力されています。
これは、someFunc(num:) をその場で await しているため、ループの中の処理はそこで suspend された状態になるのが原因です。someFunc(num:) の処理が終了してループの処理が1つ終わった後、そこからさらに指定時間(今回は約1秒)の経過を待って次のループが始まります。

n秒ごとに処理を繰り返して実行し始める

次に、someFunc(num:) にかかる処理時間に依らず、指定時間ごとにループ中の処理が開始されるようにコードを見直します。

import Foundation

let timer = AsyncStream {
    try? await Task.sleep(for: .seconds(1))
}

var num = 0
let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        print("BEGIN", currentTime)
        for await _ in timer {
            num += 1
            print(num, "FIRED", currentTime)
            group.addTask {
                await someFunc(num: num)
            }
        }
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 2 FIRED 0:00:02
 2 START 0:00:02
 3 FIRED 0:00:03
 3 START 0:00:03
 1 FINISH 0:00:03
 4 FIRED 0:00:04
 4 START 0:00:04
 2 FINISH 0:00:04
 5 FIRED 0:00:05
 5 START 0:00:05
 3 FINISH 0:00:05
 6 FIRED 0:00:06
 6 START 0:00:06
 4 FINISH 0:00:06
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

for-await-in ループを withTaskGroup(of:returning:body:) で包んであげるようにしました。こうすることで、"FIRED" が約1秒ごとに出力されているのが見え、someFunc(num:) はそれぞれにかかる処理時間に依らずに並行に処理されているようすが見えます。

【おまけ】AsyncStream(unfolding:onCancel:)Clock.sleep(for:tolerance:) を組み合わせて使う

AsyncStream(unfolding:onCancel:) と Clock.sleep(for:tolerance:) を組み合わせて使う
Swift のバージョン 対応環境
Swift 5.7+ iOS 16.0+, macOS 13.0+, tvOS 16.0+, watchOS 9.0+, visionOS 1.0+, Linux (Swift 5.7.1+), Windows (Swift 5.7.1+)

ひとつ前に述べた「AsyncStream(unfolding:onCancel:)Task.sleep(for:tolerance:clock:) を組み合わせて使う」で、Task.sleep(for:tolerance:clock:) を用いている部分は Clock.sleep(for:tolerance:) を用いることもできます。

以下は Clock に適合している ContinuousClock を用いた例です。

処理と処理の間をn秒空けて繰り返し実行する

import Foundation

let timer = AsyncStream {
    try? await ContinuousClock().sleep(for: .seconds(1))
}

var num = 0
let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        num += 1
        print(num, "FIRED", currentTime)
        await someFunc(num: num)
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 1 FINISH 0:00:03
 2 FIRED 0:00:04
 2 START 0:00:04
 2 FINISH 0:00:06
 3 FIRED 0:00:07
 3 START 0:00:07
 3 FINISH 0:00:09
 4 FIRED 0:00:10
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

n秒ごとに処理を繰り返して実行し始める

import Foundation

let timer = AsyncStream {
    try? await ContinuousClock().sleep(for: .seconds(1))
}

var num = 0
let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        print("BEGIN", currentTime)
        for await _ in timer {
            num += 1
            print(num, "FIRED", currentTime)
            group.addTask {
                await someFunc(num: num)
            }
        }
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 2 FIRED 0:00:02
 2 START 0:00:02
 3 FIRED 0:00:03
 3 START 0:00:03
 1 FINISH 0:00:03
 4 FIRED 0:00:04
 4 START 0:00:04
 2 FINISH 0:00:04
 5 FIRED 0:00:05
 5 START 0:00:05
 3 FINISH 0:00:05
 6 FIRED 0:00:06
 6 START 0:00:06
 4 FINISH 0:00:06
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

AsyncStream(unfolding:onCancel:)Task.sleep(nanoseconds:) を組み合わせて使う

Swift のバージョン 対応環境
Swift 5.5+ iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+, Linux, Windows

AsyncStream(unfolding:onCancel:)Task.sleep(nanoseconds:) を組み合わせて使う方法です。

AsyncStreamAsyncSequence に適合しているため、for-await-in ループによって「n秒ごとにループの中が呼ばれる」を実現できます。

currentTime の実装
import Foundation

/// 現在時刻(0時12分34秒なら `"0:12:34"`)
var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
import Foundation

let timer = AsyncStream {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
}

let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        print("FIRED", currentTime)
    }
}

/*
 BEGIN 0:00:00
 FIRED 0:00:01
 FIRED 0:00:02
 FIRED 0:00:03
 FIRED 0:00:04
 ... と約1秒ごとに出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

この方法でうれしいのは、タイマー処理が書かれている for-await-in ループはすでに Concurrency をサポートしている環境下にあるので、そのまま async なメソッド等を呼び出すことが可能なことです。

ここで、処理に約2秒を要する someFunc(num:) を用意します。

処理に約2秒かかる someFunc(num:) の実装
import Foundation

/// 処理に約2秒かかる非同期処理
func someFunc(num: Int) async {
    /// 現在時刻(0時12分34秒なら `"0:12:34"`)
    var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
    
    print(num, "START", currentTime)
    do {
        try await Task.sleep(for: .seconds(2))
        print(num, "FINISH", currentTime)
    } catch {
        print(num, "CANCELLED", currentTime)
    }
}

処理と処理の間をn秒空けて繰り返し実行する

AsyncStreamproduce の間隔を、Task.sleep(nanoseconds:) を用いて約1秒に設定し、以下のようなコード例の挙動を見てみましょう。

import Foundation

let timer = AsyncStream {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
}

var num = 0
let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer {
        num += 1
        print(num, "FIRED", currentTime)
        await someFunc(num: num)
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 1 FINISH 0:00:03
 2 FIRED 0:00:04
 2 START 0:00:04
 2 FINISH 0:00:06
 3 FIRED 0:00:07
 3 START 0:00:07
 3 FINISH 0:00:09
 4 FIRED 0:00:10
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

実際の出力を見ると約3秒ごとに "FIRED" が出力されています。
これは、someFunc(num:) をその場で await しているため、ループの中の処理はそこで suspend された状態になるのが原因です。someFunc(num:) の処理が終了してループの処理が1つ終わった後、そこからさらに指定時間(今回は約1秒)の経過を待って次のループが始まります。

n秒ごとに処理を繰り返して実行し始める

次に、someFunc(num:) にかかる処理時間に依らず、指定時間ごとにループ中の処理が開始されるようにコードを見直します。

import Foundation

let timer = AsyncStream {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
}

var num = 0
let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        print("BEGIN", currentTime)
        for await _ in timer {
            num += 1
            print(num, "FIRED", currentTime)
            group.addTask {
                await someFunc(num: num)
            }
        }
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 2 FIRED 0:00:02
 2 START 0:00:02
 3 FIRED 0:00:03
 3 START 0:00:03
 1 FINISH 0:00:03
 4 FIRED 0:00:04
 4 START 0:00:04
 2 FINISH 0:00:04
 5 FIRED 0:00:05
 5 START 0:00:05
 3 FINISH 0:00:05
 6 FIRED 0:00:06
 6 START 0:00:06
 4 FINISH 0:00:06
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

for-await-in ループを withTaskGroup(of:returning:body:) で包んであげるようにしました。こうすることで、"FIRED" が約1秒ごとに出力されているのが見え、someFunc(num:) はそれぞれにかかる処理時間に依らずに並行に処理されているようすが見えます。

Timer.TimerPublisher を使う

条件 Swift のバージョン 対応環境
values 使用 Swift 5.5+ iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, visionOS 1.0+, Linux, Windows
values 未使用、async 考慮 Swift 5.5+ iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+, Linux, Windows
values 未使用、async 考慮外 Swift 5.1+ iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+, Linux, Windows

Xcode 11 から登場した Combine のちからを借り、Timer.TimerPublisher を使う方法です。

Timer.TimerPublisherConnectablePublisherPublisher)に適合しているため、sink(receiveValue:)receiveValue クロージャで「n秒ごとにループの中が呼ばれる」を実現できます。

currentTime の実装
import Foundation

/// 現在時刻(0時12分34秒なら `"0:12:34"`)
var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
import Combine
import Foundation

let timer = Timer
    .publish(every: 1, on: .current, in: .common)
    .autoconnect()

print("BEGIN", currentTime)
let cancellable = timer.sink { _ in
    print("FIRED", currentTime)
}

/*
 BEGIN 0:00:00
 FIRED 0:00:01
 FIRED 0:00:02
 FIRED 0:00:03
 FIRED 0:00:04
 ... と約1秒ごとに出力される
 */

// タイマーを止めたいときは
cancellable.cancel()

しかし、このままでは async なメソッド等を呼び出すことができないため、Taskvalues を用いる方法を考えます(環境・条件の制約によって values が使用できない…… という場合のコード例は省略します)。

ここで、処理に約2秒を要する someFunc(num:) を用意します。

処理に約2秒かかる someFunc(num:) の実装
import Foundation

/// 処理に約2秒かかる非同期処理
func someFunc(num: Int) async {
    /// 現在時刻(0時12分34秒なら `"0:12:34"`)
    var currentTime: String { Date.now.formatted(date: .omitted, time: .standard) }
    
    print(num, "START", currentTime)
    do {
        try await Task.sleep(for: .seconds(2))
        print(num, "FINISH", currentTime)
    } catch {
        print(num, "CANCELLED", currentTime)
    }
}

処理と処理の間をn秒空けて繰り返し実行する

Timer.TimerPublisher の間隔を約1秒に設定し、以下のようなコード例の挙動を見てみましょう。

import Combine
import Foundation

let timer = Timer
    .publish(every: 1, on: .current, in: .common)
    .autoconnect()

var num = 0
let timerTask = Task {
    print("BEGIN", currentTime)
    for await _ in timer.values {
        num += 1
        print(num, "FIRED", currentTime)
        await someFunc(num: num)
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 1 FINISH 0:00:03
 2 FIRED 0:00:04
 2 START 0:00:04
 2 FINISH 0:00:06
 3 FIRED 0:00:07
 3 START 0:00:07
 3 FINISH 0:00:09
 4 FIRED 0:00:10
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

実際の出力を見ると約3秒ごとに "FIRED" が出力されています。

これは、someFunc(num:) をその場で await しているため、ループの中の処理はそこで suspend された状態になるのが原因です。someFunc(num:) の処理が終了してループの処理が1つ終わった後、そこからさらに指定時間(今回は約1秒)の経過を待って次のループが始まります。

n秒ごとに処理を繰り返して実行し始める

次に、someFunc(num:) にかかる処理時間に依らず、指定時間ごとにループ中の処理が開始されるようにコードを見直します。

import Combine
import Foundation

let timer = Timer
    .publish(every: 1, on: .current, in: .common)
    .autoconnect()

var num = 0
let timerTask = Task {
    await withTaskGroup(of: Void.self) { group in // または withThrowingTaskGroup
        print("BEGIN", currentTime)
        for await _ in timer.values {
            num += 1
            print(num, "FIRED", currentTime)
            group.addTask {
                await someFunc(num: num)
            }
        }
    }
}

/*
 BEGIN 0:00:00
 1 FIRED 0:00:01
 1 START 0:00:01
 2 FIRED 0:00:02
 2 START 0:00:02
 3 FIRED 0:00:03
 3 START 0:00:03
 1 FINISH 0:00:03
 4 FIRED 0:00:04
 4 START 0:00:04
 2 FINISH 0:00:04
 5 FIRED 0:00:05
 5 START 0:00:05
 3 FINISH 0:00:05
 6 FIRED 0:00:06
 6 START 0:00:06
 4 FINISH 0:00:06
 ... と出力される
 */

// タイマーを止めたいときは
timerTask.cancel()

for-await-in ループを withTaskGroup(of:returning:body:) で包んであげるようにしました。こうすることで、"FIRED" が約1秒ごとに出力されているのが見え、someFunc(num:) はそれぞれにかかる処理時間に依らずに並行に処理されているようすが見えます。

なお、Timer.TimerPublisher を作る際に用いている publish(every:tolerance:on:in:options:) には RunLoop を受け取る引数がありますが、RunLoop は通常スレッドセーフではなく、Swift Concurrency と相性があまりよくないと考えています[1:1]

むすび

個人的には

  1. AsyncTimerSequence を使う
  2. AsyncStream(unfolding:onCancel:)Task.sleep(for:tolerance:clock:) を組み合わせて使う
  3. Timer.TimerPublisher を使う

の優先順位で採用したいなと思いました。AsyncTimerSequence はまさにこのような用途のために用意されているだけあって、使い勝手が良いです。

脚注
  1. 例ではクラスプロパティの current を用いており、これは非同期コンテキストから使用できないので Swift 6 ではエラーになります(main は使用可能) ↩︎ ↩︎

Discussion