1. 概述

在开发过程中,我们经常需要执行周期性任务,比如定时更新数据、传感器监控、发送通知等。这类需求在各类应用中都非常常见。

本文将介绍几种在 Kotlin 中实现重复任务调度的方式,包括使用 Java 提供的 TimerScheduledExecutorService,以及 Kotlin 自带的协程(Coroutines)和 Flow。

我们将会看到每种方式的使用方式、适用场景以及需要注意的地方。对于有经验的开发者来说,这是一份实用的参考指南。


2. 使用 Timer.schedule()

Timer 是 Java 中 java.util 包下的一个类,可以用于定时执行任务,也可以周期性执行任务:

val timer = Timer()

每个 Timer 实例都在一个后台线程中顺序执行任务。如果某个任务执行时间过长,会影响后续任务的执行。因此,任务本身应尽量轻量。

我们可以通过 schedule() 方法创建一个定时任务:

timer.schedule(object : TimerTask() {
    override fun run() {
        println("Timer ticked!")
    }
}, 0, 1000)

Kotlin 提供了更简洁的封装方式,可以直接使用 Lambda:

timer.schedule(0L, 1000L) {
    println("Timer ticked!")
}

最后,如果需要停止任务,调用 cancel() 方法即可:

timer.cancel()

优点:简单易用
缺点:单线程执行,任务异常会中断后续任务


3. 使用 ScheduledExecutorService

ScheduledExecutorServicejava.util.concurrent 包中的接口,允许我们:

  • 定时执行任务
  • 周期性执行任务

它通过线程池来执行任务,因此比 Timer 更高效:

val scheduler = Executors.newScheduledThreadPool(1)

我们使用 scheduleAtFixedRate() 方法来执行周期性任务:

scheduler.scheduleAtFixedRate({
    println("Complex task completed!")
}, 0, 1, TimeUnit.SECONDS)

当不再需要执行任务时,记得调用 shutdown() 关闭线程池:

scheduler.shutdown()

优点:支持多线程、更灵活的调度机制
缺点:需要手动管理线程池生命周期


4. 使用协程(Coroutines)

Kotlin 协程是一种轻量级的并发模型,非常适合处理异步任务,也适用于周期性任务的调度。

4.1. 使用 repeat()delay()

Kotlin 提供了两个非常实用的函数:

  • repeat(n):重复执行某段代码 n 次
  • delay(time):延迟执行当前协程,不阻塞线程

示例代码如下:

var count = 0
repeat(10) {
    count++
    println("Timer ticked! $count")
    delay(1000.milliseconds)
}

assertEquals(10, count)

这段代码会执行 10 次打印,并在每次之间延迟 1 秒。

4.2. 使用 withTimeout()

withTimeout() 可用于限制协程执行的最大时间,超时后会抛出 TimeoutCancellationException

var count = 0
assertThrows<TimeoutCancellationException> {
    withTimeout(5000.milliseconds) {
        while (true) {
            count++
            println("Waiting for timeout")
            delay(1000.milliseconds)
        }
    }
}
assertEquals(5, count)

⚠️ 注意:使用 withTimeout() 时,一定要配合 try-catch 或者在测试中使用 assertThrows

优点:非阻塞、轻量级、支持异常处理
缺点:需要理解协程上下文和生命周期管理


5. 使用协程 Flow

Kotlin Flow 是基于协程构建的响应式流处理库,非常适合处理周期性、流式数据任务。

Flow 是 冷流(Cold Flow),只有在调用 collect() 时才会开始执行。

我们可以构建一个每秒发射一次数据的 Flow:

val flow = flow {
    while (true) {
        emit(Unit)
        delay(1000.milliseconds)
    }
}

5.1. 使用 collect()

collect() 是 Flow 中用于消费数据流的方法:

var count = 0

flow.collect {
    count++
    println(count)
}

这段代码会无限打印递增的数字,每秒一次。

5.2. 使用 take()collect()

如果我们只想执行一定次数,可以使用 take()

flow.take(10).collect {
    count++
    println("Task executed $count")
}

这段代码只会执行 10 次任务,之后自动结束。

优点:适合处理周期性数据流、可组合性强
缺点:需要理解 Flow 的生命周期和取消机制


6. 总结

我们介绍了几种在 Kotlin 中实现周期性任务调度的方式:

方法 适用场景 特点
Timer 简单任务 单线程,容易踩坑
ScheduledExecutorService 多线程任务 灵活但需手动管理
Coroutines 协程任务 非阻塞、轻量级
Flow 流式任务 适合数据流处理

对于简单任务,Timer 足够使用;如果任务复杂度上升,建议使用 ScheduledExecutorService 或协程;如果涉及流式数据或周期性数据处理,推荐使用 Flow

所有示例代码都可以在 GitHub 仓库 中找到。

建议:优先使用协程和 Flow,避免使用 Timer,因为其存在单线程、异常中断等问题,容易成为潜在的坑点。


原始标题:Scheduling Repeating Task in Kotlin