kotlin协程概述

322 阅读6分钟

概述

一个简单的线程框架。

优点

  • 用看起来同步的方式,写出异步的代码,实现非阻塞式挂起。
  • 可以把运行在不同线程的代码,写在一个代码块里,避免回调地狱。

最常见解决场景

比如A和B2个网络请求,都成功后才进行展示数据,但是如果使用原来回调来写,这2个必须是串行的,才能保证都得到了结果,这个写法就很垃圾。

协程可以把复杂的并行代码,写的更简单。

使用时机

当需要切线程或者指定线程时。

挂起

  • 执行挂起时,协程就和线程脱离了,当前线程该干嘛干嘛,协程在指定线程里执行,执行完再切回来
  • 挂起=稍后会被切回来的线程切换
  • 挂起需要恢复,恢复是在协程里,所以挂起函数必须在协程里调用或者另一个挂起函数中调用。
  • withContext就是挂起函数。
  • suspend没有挂起作用,只是说这个函数是挂起函数,是提醒作用,是函数创建者对函数调用者的提醒,提醒我这个函数是耗时操作,需要在协程在运行。

非阻塞式挂起

非阻塞式就是上下2行代码可以在线程切走再切回来而已。

一些小例子

  • 启动协程的是协程启动器,协程启动器不多,就几个,其中launch用的最多。
  • launch是协程作用域的扩展方法,所以使用launch必须在协程作用域里。
  • 一旦开启协程,就和主线程脱离了关系,主线程该干啥干啥去。
lifecycleScope.launch {
    Log.i(TAG, "initView: 我要延迟")
    Log.i(TAG, "initView: 我是协程")
    delay(10000)
    Log.i(TAG, "initView: 延迟结束")
    launch {
        Log.i(TAG, "initView: 我又开启了协程")
    }
}
Log.i(TAG, "initView: 主线程继续跑别的")
  • 使用runBlocking也可以开启协程,但是这个不是协程扩展函数。
  • 既然它不是协程扩展函数,所以它会卡住当前线程。
runBlocking {
    Log.i(TAG, "initView: 我可以卡主线程")
    delay(5000)
    Log.i(TAG, "initView: 延迟结束 好像gg 界面卡死")
}
  • delay也是挂起函数。
  • 定义挂起函数必须要使用suspend关键字,说明我这个是挂起函数,必须在协程里调用,谢谢。
private suspend fun delay10k(){
    delay(10000)
    Log.i(TAG, "deley10k: 延迟结束,我要开启协程")
}

既然我的挂起函数能在协程里调用,那我挂起函数再开启协程也合情合理喽,不过哪来的协程范围呢?这就需要看coroutineScope函数了,这个函数能创建一个协程范围从协程里,也就是子范围,还是很牛批的:

private suspend fun delay10k(){
    delay(10000)
    Log.i(TAG, "deley10k: 延迟结束,我要开启协程")
    coroutineScope {
        Log.i(TAG, "deley10k: 我也有作用域了 我要开启协程")
        launch {
            Log.i(TAG, "deley10k: 终于开启了 先延迟2s")
            delay(2000)
            Log.i(TAG, "delay10k: 延迟2s结束")
        }
    }
    Log.i(TAG, "deley10k: 那我到底是先执行 还是等你2s结束呢 ")
}

这里的coroutineScope函数很有意思,它类似runblocking可以阻塞,runBlocking是阻塞当前线程,比如我代码在主线程执行,使用runBlocking开启的协程必须要执行完再执行线程;coroutineScope函数是阻塞协程,必须要等这个子范围内的协程执行完后,原来的协程才能继续执行。

说完了launch、runBlocking启动器,还有一个启动器就是async,从名字来看就是异步的意思,果然它很牛批。就比如2个耗时操作,使用这个可以直接并发开始,然后等待都有结果再处理或者其他条件处理,比在一个方法回调里启动另一个方法强多了。

runBlocking {
   Log.i(TAG, "initView: 开始协程")
   val start = System.currentTimeMillis()
   val deferred = async {
       Log.i(TAG, "initView: 我是A耗时任务 要开始了")
       delay(1000)
       Log.i(TAG, "initView: 我是A耗时任务,我结束了")
       4 + 5
   }
   val deferred2 = async {
       Log.i(TAG, "initView: 我是B耗时任务 要开始了")
       delay(1000)
       Log.i(TAG, "initView: 我是B耗时任务,我结束了")
       8 +9
   }
   Log.i(TAG, "initView: 结果是${deferred.await()}  ${deferred2.await()}")
   val end = System.currentTimeMillis()
   Log.i(TAG, "initView: 耗时 ${end - start}")
}

这里会发现时间耗时就1002秒,2个需要耗时各1s的居然在1s多都获取了结果,这个就很nice,如果其他方式来写的话就比较麻烦。

这个async方法的await方法是个阻塞方法,这啥意思呢,就是它会等待结果,但是异步任务在创建的时候就已经在跑了,调用await时只是获取其结果。比如在await前加个耗时:

runBlocking {
    val deferred = async {
    Log.i(TAG, "initView: 开始协程")
    val start = System.currentTimeMillis()
        Log.i(TAG, "initView: 我是A耗时任务 要开始了")
        delay(1000)
        Log.i(TAG, "initView: 我是A耗时任务,我结束了")
        4 + 5
    }
    val deferred2 = async {
        Log.i(TAG, "initView: 我是B耗时任务 要开始了")
        delay(1000)
        Log.i(TAG, "initView: 我是B耗时任务,我结束了")
        8 +9
    }
    Log.i(TAG, "initView: 大家不必紧张,先暂停协程4s")
    delay(4000)
    Log.i(TAG, "initView: 结果是${deferred.await()}  ${deferred2.await()}")
    val end = System.currentTimeMillis()
    Log.i(TAG, "initView: 耗时 ${end - start}")
}

查看打印:

2021-08-24 14:19:42.252 13422-13422/com.wayeal.yunapp I/zyh: initView: 开始协程 2021-08-24 14:19:42.252 13422-13422/com.wayeal.yunapp I/zyh: initView: 大家不必紧张,先暂停协程4s 2021-08-24 14:19:42.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是A耗时任务 要开始了 2021-08-24 14:19:42.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是B耗时任务 要开始了 2021-08-24 14:19:43.255 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是A耗时任务,我结束了 2021-08-24 14:19:43.255 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是B耗时任务,我结束了 2021-08-24 14:19:46.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 结果是9 17 2021-08-24 14:19:46.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 耗时 4002

会发现其实A B任务已经结束了,在调用await时才获取的值。

下面介绍一个非常重要的函数是withContext函数,它相当于是async+await结合在一块,其实也是启动协程的方法,把它看成协程启动器也不为过,但是必须指定调度器,而且这个方法会阻塞当前协程,这个特别关键,如果要异步还是得用async,直接看例子:

runBlocking {
    Log.i(TAG, "initView: 开始协程")
    val start = System.currentTimeMillis()
    val result1 = withContext(Dispatchers.Default){
        Log.i(TAG, "initView: 我是任务A 我要开始了")
        delay(1000)
        Log.i(TAG, "initView: 我是任务A 我结束了")
        5 + 10
    }
    val result2 = withContext(Dispatchers.Default){
        Log.i(TAG, "initView: 我是任务B 我要开始了")
        delay(1000)
        Log.i(TAG, "initView: 我是任务B 我结束了")
        9 + 1
    }
    Log.i(TAG, "initView: 结果是${result1}  ${result2}")
    val end = System.currentTimeMillis()
    Log.i(TAG, "initView: 耗时 ${end - start}")
}

打印结果: 2021-08-24 14:30:43.481 13611-13611/com.wayeal.yunapp I/zyh: initView: 开始协程 2021-08-24 14:30:43.485 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务A 我要开始了 2021-08-24 14:30:44.487 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务A 我结束了 2021-08-24 14:30:44.488 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务B 我要开始了 2021-08-24 14:30:45.489 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务B 我结束了 2021-08-24 14:30:45.489 13611-13611/com.wayeal.yunapp I/zyh: initView: 结果是15 10 2021-08-24 14:30:45.489 13611-13611/com.wayeal.yunapp I/zyh: initView: 耗时 2008

从打印结果可以发现,先执行的A 再执行的B,2个耗时任务是串行的。

总结

协程的东西其实并不多,先了解其就是一个线程操作的API库,然后挂起就是切线程,执行完还会给切回来这个本质就好了。关于启动协程就是launch、async、runBlocking、withContext、coroutineScope等方法的使用,根据具体业务调用方法即可。