Kotlin系列七:协程总结

1,490 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一 协程基本用法

协程:协程允许我们在单线程模式下模拟多线程编程效果,代码执行时的挂起与恢复完全由编程语言控制,和操作系统无关。这种特性使得高并发程序的运行效率得到极大的提升,可以用看起来同步的代码写出实际上异步的操作。

Kotlin没有将协程纳入标准库的API中,而是以依赖库的形式提供的。以安卓为例:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

1.1 GlobalScope

GlobalScope.launch 每次创建的都是一个顶层协程,当前应用程序结束协程跟着结束

//此时只会打印codes run is coroutine scope,
//不会打印codes run is coroutine scope finished,
//因为main结束了会强制结束当前协程
fun main(){
    GlobalScope.launch {
        println("codes run is coroutine scope")
        delay(1500)
        println("codes run is coroutine scope finished")
    }
    Thread.sleep(1000)
}

1.2 runBlocking

runBlocking函数也会创建一个协程的作用域,与GlobalScope.launch 不同的是,它可以保证在协程作用域内的所有代码和子协程没有执行完成之前一直阻塞当前线程。

因为它可能会阻塞当前线程,而你又恰好在主线程中调用它,就有可能卡死界面,因此该函数只推荐在测试环境下使用,否则会影响性能。

//此时只会打印codes run is coroutine scope和codes run is coroutine scope finished,
//因为runBlocking 会在协程内的代码和子协程未完成的情况下阻塞线程
fun main(){
    runBlocking {
        println("codes run is coroutine scope")
        delay(1500)
        println("codes run is coroutine scope finished")
    }
    Thread.sleep(1000)
}

1.3 launch

launch函数只能在协程作用域中才能被调用,且它会在当前协程作用域下创建子协程。子协程会随外层作用域协程的结束而一同结束。

//打印顺序launch1 launch2 launch1 finished launch2 finished
//子作用域可以并发执行,并且根据编程语言先后执行顺序
fun main(){
    runBlocking {
        launch{
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch{
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
    Thread.sleep(1000)
}

1.4 suspend

如果在launch函数中调用一个其他函数会产生一个问题:launch函数中的代码是有协程作用域的,但是抽取到一个单独函数中的代码就没有协程作用域了,无法调用类似于delay()这种挂起函数。

解决办法:加suspend关键字,它可以将任意函数声明成挂起函数,而挂起函数是可以相互调用的。

//声明成挂起函数,挂起函数可以相互调用的
suspend fun printDot(){
    println(".")
    delay(1000)
}

1.5 coroutineScope

suspend关键字只能声明为挂起函数,无法提供作用域,因此1.4中的问题只解决了挂起函数相互调用的问题,被调用的函数中还是没有协程作用域,无法调用类似于launch()函数。此时就用到了coroutineScope 函数。

coroutineScope 是挂起函数,它可以继承外部作用域创建子作用域。coroutineScope 只会阻塞当前的协程,不会阻塞线程,runBlocking 会阻塞当前线程

//由于suspend 只能声明为挂起函数,无法提供作用域
//coroutineScope 是挂起函数,并且可以继承外部作用域创建子作用域
//coroutineScope 只会阻塞当前的协程,不会阻塞线程,runBlocking 会阻塞当前线程
suspend fun printDot() = coroutineScope {
    launch{
        println(".")
        delay(1000)
    }
}

1.6 小结

GlobalScope.launch和runBlocking函数可以再任意地方调用;

coroutineScope函数可以在协程作用域或挂起函数中调用;

launch函数只能在协程作用域中调用;

可创建新协程作用域的函数:

  • GlobalScope.launch 可在任何地方调用

  • runBlocking 可在任何地方调用

  • lanuch

  • coruotineScope

二 更多的作用域构建器

1.6中已经介绍了四种可创建新协程作用域的函数,但是比如像GlobalScope.launch()函数,它每次都是创建的顶层协程,管理成本高(要一个个去job.cancel),所以除非明确要简历顶层协程,否则不建议使用GlobalScope.launch()。

//取消当前协程
val job = GlobalScope.launch {
    //处理具体逻辑
}
job.cancel()

//GlobalScope顶层协议,多个协议需要一个一个取消不方便
//CoroutineScope可以实现一个一个取消
val job = Job()
val scope = CoroutineScope(job)
scope.launch {

}
job.cancel()

launch函数返回值永远是一个Job对象,利用async函数可以创建一个协程并获取它的执行结果。async函数必须在协程作用域中才能调用,它会创建一个新子协程并返回一个Deferred对象,调用deferred.await()方法就可获得async函数代码块的结果。

withContext()函数可以理解成async函数的简化版写法,但是它强制要求指定一个线程参数。线程参数主要有3种可选:

  1. Dispatchers.Default:默认的低并发线程策略,适用于计算密集型任务。

  2. Dispatchers.IO:较高并发线程策略,适用于主要在阻塞和等待中的代码。

  3. Dispatchers.Main:只能在安卓项目中使用,在Android主线程中执行。

//获取协程内容返回值
fun main(){
    runBlocking {
        val result = async {
            5+5
        }.await()//后面的代码会在有返回值后才执行,所以有多个async请在最后执行await,不然会串行执行,效率很低
        println(result)
    }
}
 
//相当于上面代码
fun main(){
    runBlocking {
        val result = withContext(Dispatchers.Default){
            5+5
        }
        println(result)
    }
}

三 协程简化回调

suspendCoroutine函数几乎可以用于简化任何回调写法。

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)
            }
        })
    }
}
 
GlobalScope.launch {
    val response = request("https://www.baidu.com/")
    Log.d("xfhy", "网络请求结果 : $response")
}
 

将网络请求的代码使用suspendCoroutine包装一下,即可免去每次去手动生成一个匿名类。调用continuation的resume方法将结果返回,可以在外面以看上去同步的代码拿到请求结果。上面为了简单,没有加try…catch,实际中需要加上捕获异常。

Retrofit网络请求也可以简化:

val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object:Callback<List<App>>) {
    
    override fun onResponse(call:Callback<List<App>>,response:Response<List<App>>) {
        //得到服务器返回的数据
    }
 
    override fun onFailure(call:Callback<List<App>>,t:Throwble) {
        //对异常进行处理
    }
 
}
 
//suspendCoroutine函数可以简化上面的回调
//suspendCoroutine必须在协程作用域或者挂起函数中调用,他接受lambda表达式
suspend fun <T> Call<T>.await():T{
    return suspendCoroutine { continuation ->
        enqueue(object:Callback<List<App>>) {
    
            override fun onResponse(call:Callback<List<App>>,response:Response<List<App>>) {
                val body = response.body()
                if(body!=null)
                    continuation.resume(body)
                else
                    continuation.resumeWithException(RuntimeExceotion("respnse body is null")) 
            }
 
            override fun onFailure(call:Callback<List<App>>,t:Throwble) {
                //对异常进行处理
                continuation.resumeWithException(t) 
            }
    }
}
 
suspend fun getAppData(){
    try {
        val appList = ServiceCreator.creat<AppService>().getAppData().await()
        //对服务器响应数据进行处理
    } catch (e:Exception) {
        //对异常做处理
    }
}
 
// 上述挂起函数可以在liveData()函数中调用,
// 因为liveData()函数中提供一个挂起函数的上下文,
// 并自动构建返回一个LivaData对象,
// emit()类似于LiveData的setValue()函数通知数据变化
object Respository {
 
    fun searchPlaces(query:String) = liveData(Dispatchers.IO){
        val result = try{
            val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
            if(placeResponse.status == "ok"){
                val places = placeResponse.places
                Result.success(places)
            }else{
                Result.failure(RuntimeExceotion("response status is ${placeResponse.status}"))
            }
        }catch(e:Exception){
            Result.failure<List<Place>>(e)
        }
        emit(result)
    }    
 
}

主要例子和参考

郭霖《第一行代码》 Kotlin部分