在Kotlin中,挂起函数(suspend function)是协程(Coroutine)编程中的一个核心概念。它们提供了一种简洁而强大的方式来编写异步代码,同时保持代码的可读性和顺序性。
下面对Kotlin中挂起函数进行分析。
什么是挂起函数?
挂起函数是用suspend关键字修饰的函数。这意味着该函数可以在执行过程中挂起其执行,并让出CPU资源给其他任务,而不会阻塞调用它的线程。当挂起函数恢复执行时,它会从挂起点继续执行,并返回结果。
挂起函数的特点
非阻塞性:
挂起函数不会阻塞调用它的线程。这意味着即使函数内部有耗时操作(如网络请求或数据库访问),也不会导致调用线程被阻塞。
协程上下文:
挂起函数只能在协程的上下文中调用。它们不能在普通的线程或主线程上直接调用,需要启动一个协程来调用它们。
自动恢复:
当挂起函数被挂起时,Kotlin协程框架会自动保存其执行状态。一旦挂起的原因被解决(例如,等待的异步操作完成),协程框架会自动恢复挂起函数的执行。
返回值:
挂起函数可以有返回值,也可以没有返回值(Unit类型)。返回值的类型可以是任意类型,包括其他挂起函数。
fun main() {
// 定义一个挂起函数,模拟网络请求
suspend fun fetchDataFromNetwork(): String {
// delay 是 Kotlin 协程库中的一个挂起函数,用于模拟耗时操作。这里它会让当前协程暂停 1 秒钟,但不会阻塞线程。
delay(1000) // 模拟耗时操作,非阻塞延迟 1 秒(挂起函数)
return "Data from network"
}
// runBlocking是一个协程构建器,它会阻塞当前线程(通常是主线程),直到其内部的协程执行完毕。
// 它主要用于在非协程环境(如 main 函数)中启动协程。
runBlocking { // 创建一个顶层协程作用域
// 挂起函数必须由协程调用,协程通过 launch 或 async 启动。
// 在 runBlocking 协程作用域内,直接调用了 fetchDataFromNetwork 挂起函数。
// 由于 runBlocking 本身就创建了一个协程上下文,因此可以直接调用挂起函数。
val data = fetchDataFromNetwork() // 在协程的上下文中调用挂起函数
println(data)
}
}
在上面的代码中,为什么没有在协程作用域中调用launch或async启动新协程呢? 因为不需要额外的并发执行,launch 用于在协程作用域内启动一个新的协程,实现并发执行。在这段代码中,runBlocking 已经创建了一个协程作用域,并且我们只需要按顺序执行 fetchDataFromNetwork 函数,获取数据并打印结果,不需要额外的并发操作。直接在 runBlocking 内部调用 fetchDataFromNetwork 就可以满足需求,所以不需要使用 launch 启动新的协程。
// 挂起函数 1
suspend fun fetchData(): String {
delay(1000)
return "Fetched data"
}
// 挂起函数 2(切换线程)
suspend fun loadFromNetwork(): String {
// withContext(Dispatchers.IO):withContext 是一个挂起函数,用于在指定的调度器上下文中执行代码块。
// Dispatchers.IO 是一个调度器,专门用于执行 I/O 密集型任务,例如网络请求、文件读写等。
// 使用 Dispatchers.IO 可以避免阻塞主线程。
return withContext(Dispatchers.IO) {
delay(1000)
"Network result"
}
}
// 挂起函数 3(模拟数据库操作)
suspend fun loadFromDatabase(): String {
// 使用 withContext(Dispatchers.IO) 将数据库操作切换到 I/O 线程执行,避免阻塞主线程。
return withContext(Dispatchers.IO) {
delay(500)
"Database result"
}
}
fun main() {
println("========================")
// runBlocking是一个协程构建器,它会阻塞当前线程(通常是主线程),直到其内部的协程执行完毕。
// 它主要用于在非协程环境(如 main 函数)中启动协程。
runBlocking {
// 示例 1:挂起函数与协程
val job = launch { // launch:用于在 runBlocking 协程作用域内启动一个新的协程。
val data = fetchData() // 在新协程中调用挂起函数 fetchData,获取数据并打印。
println("Data: $data")
}
// job.join():等待协程执行完毕,确保在协程完成后才继续执行后续代码。
job.join() // 等待协程完成
// 示例 2:协程作用域与结构化并发
// CoroutineScope(Dispatchers.Default):创建一个新的协程作用域,并指定调度器为 Dispatchers.Default,
// 该Dispatchers.Default调度器适用于执行 CPU 密集型任务。
val scope = CoroutineScope(Dispatchers.Default)
// scope.launch:在新的协程作用域内启动一个协程。
scope.launch {
// async:用于在协程中启动异步任务,返回一个 Deferred 对象。
// Deferred 对象可以通过 await 方法获取异步任务的结果。
val deferred1 = async { loadFromNetwork() }
val deferred2 = async { loadFromDatabase() }
// result1.await() 和 result2.await():等待异步任务完成并获取结果。
println("Results: ${deferred1.await()}, ${deferred2.await()}")
}
delay(1500) // 暂停当前协程 1.5 秒钟,确保异步任务有足够的时间完成。
scope.cancel() // 取消作用域内的所有协程,释放资源。
}
}
下面看另一个代码示例:
suspend fun getA(): String {
println("in getA before delay")
withContext(Dispatchers.IO) {
delay(5000L)
// 下面一行代码输出:now in A IO process:Thread[#30,DefaultDispatcher-worker-1,5,main]
println("now in A IO process:" + Thread.currentThread())
}
// 下面一行代码输出: finish A process:Thread[#1,main,5,main]
println("finish A process:" + Thread.currentThread())
return "A content"
}
suspend fun getB(a: String): String {
println("in getB before delay")
withContext(Dispatchers.IO) {
delay(2000L)
// 下面一行代码输出:now in B IO process:Thread[#30,DefaultDispatcher-worker-1,5,main]
println("now in B IO process:" + Thread.currentThread())
}
// 下面一行代码输出:finish B process:Thread[#1,main,5,main]
println("finish B process:" + Thread.currentThread())
return "$a B content"
}
suspend fun getC(b: String): String {
println("in getC before delay")
withContext(Dispatchers.IO) {
delay(2000L)
// 下面代码输出:now in C IO process:Thread[#32,DefaultDispatcher-worker-3,5,main]
println("now in C IO process:" + Thread.currentThread())
}
// 下面代码输出: finish C process:Thread[#1,main,5,main]
println("finish C process:" + Thread.currentThread())
return "$b C content"
}
fun main() {
runBlocking {
val a = getA()
println(a)
val b = getB(a)
println(b)
val c = getC(b)
println(c)
}
}
综上可知,Kotlin中的挂起函数提供了一种优雅的方式来处理异步操作,使得异步代码更加简洁和易于理解。通过挂起函数,我们可以避免传统的回调地狱,并充分利用Kotlin协程的强大功能来编写高效、可维护的异步代码。