协程是一种轻量级的线程。
线程是依靠操作系统的调度而实现不同线程之间的切换。
协程仅在编程语言层面就能实现不同协程之间的切换,从而大大提高了并发编程的运行效率。
协程允许在单线程模式下模拟多线程编程的效果,代码执行时的挂起于恢复完全由编程语言来控制,与操作系统无关。
基本用法
首先要在app/build.gradle
中添加依赖库
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
开启协程
fun main(){
GlobalScope.launch {
println("codes run in coroutine scope")
}
Thread.sleep(1000)
}
运行主函数就会看到打印的日志
GlobalScope.launch
函数可以创建一个协程的作用域,这样传递给launch
函数的代码块(Lambda表达式)就是在协程中运行的了,这里只是在代码块中打印了一行日志。
但是如果将Thread.sleep(1000)
语句删掉,就不能成功打印了。这是因为,Global.launch
函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。刚才的日志之所以无法打印出来,就是因为代码块中的代码还没来得及运行,应用程序就结束了。而Thread.sleep()
方法让主线程阻塞1秒钟,就有时间成功打印。
runBlocking函数
在大部分情况下,是不可能每次都计算好协程所需的时间然后让主线程阻塞等待的。这时就需要借助到runBlocking
函数,他可以让应用程序在协程中所有代码都运行完了之后再结束。
fun main(){
runBlocking{
println("codes run in coroutine scope")
delay(1500) //让当前协程延迟指定时间后再运行,只能在协程作用域或其他挂起函数中调用
println("codes run in coroutine scope finished")
}
}
runBlocking
函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking
函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。
创建多协程
fun main(){
runBlocking{
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
注意这里的launch
函数和刚才所使用的GlobalScope.launch
函数不同。
首先先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。
相比而言,GlobalScope.launch
函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。
可以看到,两个子协程中的日志是交替打印的,说明它们确实是像多线程那样并发运行的。然而这两个子协程实际却运行在同一个线程当中,只是由编程语言来决定如何在多个协程之间进行调度,让谁运行让谁挂起。 调度的过程完全不需要操作系统参与,这也就使得协程的并发效率会出奇得高。
suspend关键字
suspend
关键字可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。
suspend fun printDot(){
println(".")
delay(1000)
}
但是,suspend
关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如在printDot()
函数中是不能调用launch
函数的,因为launch
函数要求必须在协程作用域当中才能调用
coroutineScope函数
coroutineScope
函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,就可以给任意挂起函数提供协程作用域了。
suspend fun printDot() = coroutineScope{
println(".")
delay(1000)
launch{
}
}
另外coroutineScope
函数还可以保证在其作用域内的所有代码和子协程在全部执行完之前,外部的协程会被一直挂起。
fun main(){
runBlocking{
coroutineScope {
launch {
for (i in 1..5){
println(i)
delay(1000)
}
}
}
println("coroutineScope finished")
}
println("runBlocking finished")
}
注意
coroutineScope函数
只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。
⽽runBlocking函数
由于会挂起外部线程,如果恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。
更多作用域构建器
总结特点
名称 | 调用环境 | 特点 |
---|---|---|
GlobalScope.launch |
可以在任意地方调用 | 每次创建的都是顶层协程,不常用 |
launch |
只能在协程作用域中调用 | \ |
runBlocking |
可以在任意地方调用 | 会阻塞线程,建议只在测试环境中使用 |
coroutineScope |
可以在协程作用域或者挂起函数中调用 | \ |
项目中常用的写法
在使用协程时,还要考虑到用户提前关闭Activity时要取消协程的问题。
GlobalScope.launch
函数和launch
函数都会返回一个Job
对象,只需要调用Job
对象的cancel()
方法就可以取消协程了
val job = GlobalScope.launch{
//具体处理逻辑
}
job.cancel()
那么在实际使用中,常用的写法为:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
//具体处理逻辑
}
job.cancel()
先创建了一个Job
对象,然后把它传入CoroutineScope()
函数当中,CoroutineScope()
函数会返回一个CoroutineScope
对象,有了CoroutineScope
对象之后,就可以随时调用它的launch
函数来创建一个协程了。 现在所有调用CoroutineScope
的launch
函数所创建的协程,都会被关联在Job
对象的作用域下面。这样只需要调用一次cancel()
方法,就可以将同一作用域内的所有协程全部取消,从而降低了协程管理的成本。
async函数
async
函数可以创建一个协程并获取它的执行结果,它必须在协程作用域当中才能调用,会创建一个新的子协程并返回一个Deferred
对象,如果想要获取async
函数代码块的执行结果,只需要调用Deferred
对象的await()
方法即可。
fun main(){
runBlocking{
val result = async {
5 + 5
}.await()
print(result)
}
}
在调了async
函数之后,代码块中的代码就会立刻开始执行。当调用await()
方法时,如果代码块中的代码还没执行完,那么await()
方法会将当前协程阻塞住,直到可以获得async
函数的执行结果。
所以在使用时,不在每次调用async
函数之后就立刻使用await()
方法获取结果 了,而是仅在需要用到async
函数的执行结果时才调用await()
方法进行获取,这样多个async
函数就变成一种并行关系了,可以大大提高效率。
withContext()
withContext()
是一个挂起函数,用法上可以看作是async
函数的简化版。
fun main(){
runBlocking{
val result = withContext(Dispatchers.Default){
5 + 5
}
println(result)
}
}
调用withContext()
函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后, 会将最后一行的执行结果作为withContext()
函数的返回值返回,因此基本上相当于val result = async{ 5 + 5 }.await()
的写法。唯一不同的是,withContext()
函数强制要求我们一个线程参数,通过线程参数给协程指定一个具体运行的线程。
参数名称 | 作用 |
---|---|
Dispatchers.Default |
会使用一种默认低并发的线程策略,当要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,就可以使用这个参数 |
Dispatchers.IO |
会使用一种较高并发的线程策略,当要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,就可以使用这个参数 |
Dispatchers.Main |
不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。 |
使用协程简化回调
suspendCoroutine
函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda
表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda
表达式中的代码。Lambda
表达式的参数列表上会传入一个Continuation
参数,调用它的resume()
方法或resumeWithException()
可以让协程恢复执行。
suspend fun request(address: String): String {
return suspendCoroutine {
continuation ->
HttpUtil.sendHttpRequest(address, object : HttpCallbackListener{
override fun onFinish(response: String) {
//请求成功则恢复被挂起的协程,并传入服务器相应成功的数据
continuation.resume(response)
}
override fun onError(e: Exception) {
//请求失败则恢复被挂起的协程,并传入具体的异常原因
continuation.resumeWithException(e)
}
})
}
}
这样不管之后要发起多少次网络请求,都不需要再重复进行回调实现了。
suspend fun getBaiduResponse() {
try {
val response = request("https://www.baidu.com/")
// 对服务器响应的数据进⾏处理
} catch (e: Exception) {
// 对异常情况进⾏处理
}
}
getBaiduResponse()
是一个挂起函数,因此当它调用了request()
函数时,当前的协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。这样即使不使用回调的写法,也能够获得异步网络请求的响应数据,而如果请求失败,则会直接进入catch
语句当中。