Swift Concurrency 時代の「n秒ごとに処理を実行する」
「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
を使う方法です。
AsyncTimerSequence
は AsyncSequence
に適合しているため、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:)
を組み合わせて使う方法です。
AsyncStream
は AsyncSequence
に適合しているため、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秒空けて繰り返し実行する
AsyncStream
の produce
の間隔を、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:)
を組み合わせて使う方法です。
AsyncStream
は AsyncSequence
に適合しているため、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秒空けて繰り返し実行する
AsyncStream
の produce
の間隔を、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+, |
values 未使用、async 考慮 |
Swift 5.5+ | iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+, |
values 未使用、async 考慮外 |
Swift 5.1+ | iOS 13.0+, macOS 10.15+, tvOS 13.0+, watchOS 6.0+, visionOS 1.0+, |
Xcode 11 から登場した Combine のちからを借り、Timer.TimerPublisher
を使う方法です。
Timer.TimerPublisher
は ConnectablePublisher
(Publisher
)に適合しているため、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 なメソッド等を呼び出すことができないため、Task
と values
を用いる方法を考えます(環境・条件の制約によって 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]。
むすび
個人的には
AsyncTimerSequence
を使うAsyncStream(unfolding:onCancel:)
とTask.sleep(for:tolerance:clock:)
を組み合わせて使うTimer.TimerPublisher
を使う
の優先順位で採用したいなと思いました。AsyncTimerSequence
はまさにこのような用途のために用意されているだけあって、使い勝手が良いです。
Discussion