Kotlin 协程由浅入深

在这里插入图片描述

任何UI框架都有自己的主线程来进行渲染界面和观察触摸事件,这个线程绝对是你应该关注的最重要的线程,用户永远不知道你是否使用了AsyncTask或者Coroutine来编写你的代码,但是用户却可以感受到你的应用的响应速度,因此要如何组织应用所要执行的任务是你要最应该了解的事情之一

  1. HelloWorld

    如果你有一个网络请求,耗时很久,那么它必须不能在主线程执行。如果你在Android应用里这样做,将会导致你的应用崩溃。最常见的一种解决方案是在后台线程执行网络请求,然后再回调里返回结果供UI更新(比如Retrofit最简单的使用),还有大家所熟悉的Rxjava,可以在后台线程订阅网络请求,在主线程得到请求结果(而且还是链式调用,非常nice)。但是现在你又多了一种方案,使用协程

    对于普通的函数,你可以嵌套调用,当然程序也会是顺序执行,下面举例说明我们在main函数调用funA时会发生什么

    fun main() {
        funA()
    }
    fun funA(){
        println("start...")
        funB()
        funC()
        println("....end")
    }
    fun funB(){
        println("Hello")
    }
    fun funC(){
        println("World!")
    }
    

    说了这么多了,但是上面的问题看起来很可笑,因为它太简单了,但请耐心看下去。很现实程序会顺序执行,每当需要调用另外一个函数的时候,程序都会先去执行那个函数的代码,等到执行完毕返回调用的地方继续执行后面的代码

    我们让上述的情况变得再复杂一点,我们可以想象funB和funC是执行网络请求或者从本地数据库读取数据,那它们就需要消耗一些时间。我们通过延迟函数执行来模拟这种耗时操作

    fun main() {
        //Kotlin提供的计算耗时时间的一个函数
        val timeMillis = measureTimeMillis {
            funA()
        }
        println("cost time $timeMillis")
        println("Exiting main...")
    }
    fun funA(){
        println("start....")
        funB()
        funC()
        println("....end")
    }
    fun funB(){
        Thread.sleep(1000)//线程睡眠阻塞1s
        println("Hello")
    }
    fun funC(){
        Thread.sleep(1000)
        println("World!")
    

    你应该知道上述的例子执行的结果,如下

    start....
    Hello
    World!
    ....end
    cost time 2001
    Exiting main...
    

    正如我们所想,当程序开始执行会立即执行第一行代码,1s之后输出Hello,再过1s后输出World!,然后是输出执行时间,最后main执行结束。程序耗时了2s多,我们可以把它的执行时间缩短到1s吗

    于是我们想到了并发,我们可以给funB和funC分别开启一个线程来达到目的。利用线程来处理耗时操作,Java和Andoid也提供了一些Api供我们时使用,我们在这里姑且就用最简单的线程来处理提升程序的执行效率

    fun main() {
        val timeMillis = measureTimeMillis {
            funA()
            Thread.sleep(1300)//阻塞主线程
        }
        println("cost time $timeMillis")
        println("Exiting main...")
    }
    fun funA(){
        println("start....")
        thread { funB() }
        thread { funC() }
        println("....end")
    }
    fun funB(){
        Thread.sleep(1000)//线程睡眠阻塞1s
        println("Hello")
    }
    fun funC(){
        Thread.sleep(1000)
        println("World!")
    

    输出结果如下

    start....
    ....end
    Hello
    World!
    cost time 1308
    Exiting main...
    

    我们利用线程让程序利用之前一半的时间完成了任务,但这只是理想结果,实际我们在实际中利用线程写出一个没有并发错误的代码还需要其它很多知识,另一方面线程也很耗内存,一不小心就会内存溢出OutOfMemoryError,这个时候解决问题就得从语法和资源两个角度去解决问题了,如果你不差钱,可以无休止的加内存(但这是不现实的),如果=一种完美的解决方案,我们不需要修改任何东西,就可以编写出高效的代码又可以避免出现内存溢出的情况就好了,哈哈哈。

    Kotlin 可能是比较接近这种理想情况的一种解决方案,你可以做出尽可能少的重构将你之前的同步代码变成异步的

  2. 协程

    fun main() {
        GlobalScope.launch {
            funA()
            println("current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
        }
        Thread.sleep(2300)
        println("Exiting main...current thread is ${Thread.currentThread().name}")//current thread is main
    }
    suspend fun funA() {
        println("start....current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
        funB()
        funC()
        println("....end==current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
    }
    suspend fun funB() {
        delay(1000)//线程睡眠阻塞1s
        println("current thread is ${Thread.currentThread().name}--Hello")//current thread is DefaultDispatcher-worker-1
    }
    suspend fun funC() {
        delay(1000)
        println("World!--current thread is ${Thread.currentThread().name}")//current thread is DefaultDispatcher-worker-1
    }
    

    从代码上我们对funA B C 函数前都加了suspend,使用delay代替了Threed.sleep;从执行结果看起来和我们第一个没有时使用并发工具一样,也耗时了2s多,我们利用协程是否也可以缩短执行时间到1s呢

  3. 并行

    fun main() {
        GlobalScope.launch {
            val millsCoroutine = measureTimeMillis { funA() }
            println("cost time $millsCoroutine")
        }
        Thread.sleep(2300)
        println("Exiting main")
    }
    suspend fun funA(){
        println("start...")
        println("${funB()} ${funC()}")
        println("...end")
    }
    suspend fun funB():String{
        delay(1000)
        return "Hello"
    }
    suspend fun funC():String{
        delay(1000)
        return "World!"
    }
    

    同样花费了2s时间,我们可以用Async和wait缩短运行时间,他们是协程的一个构建器,可以并行执行代码,每一段代码外面套上async{},在async里的代码块执行结束,它就会返回一个Deffered的数据结构,从而得到你期望的结果

    fun main() {
        GlobalScope.launch {
            val millsCoroutine = measureTimeMillis { funA() }
            println("cost time $millsCoroutine")
        }
        Thread.sleep(2300)
        println("Exiting main")
    }
    suspend fun funA(){
        println("start...")
        val helloDeffered = GlobalScope.async { funB() }
        val worldDeffered = GlobalScope.async { funC() }
        println("${helloDeffered.await()} ${worldDeffered.await()}")
        println("...end")
    }
    

    执行结果,现在协程以并发模式运行

    start...
    Hello World!
    ...end
    cost time 1018
    Exiting main
    
  4. 结构化并发(Structured Concurrency)

    无论你是使用常规的回调还是更优雅的Rxjava,你都必须在Activity或者Fragment生命周期结束后取消后台线程的操作,才不会可能造成的内存泄露。如果你熟悉autodispose ,它是一个开源库用来在组件被销毁时自动取消Rxjava订阅,因此你不必手动处理

    协程有Structured Concurrency的概念,对我们来说这是一个新的,陌生的名词,但是它解决了上一段提出的问题(不需要时取消后台任务),Structured Concurrency只是制定了一些标准,这些标准规定了并发程序有清晰的入口点和出口点

  5. Coroutine Scope 协程作用域

    结构化并发标准之一就是不能在协程作用域之外开启协程。我们上面的例子使用GlobalScope这个协程作用域,它是一个特殊的作用域,作用于整个应用。

    android的架构组件里都提供了默认的协程作用域,比如liveData,ViewModel,LifeCycle。因此大多时候你都不需要自己实现CoroutineScope,如果你想这样做,你只需要让你的class实现CoroutineScope接口并定义上下文

    class WelcomingScreen : Activity, CoroutineScope by CoroutineScope(Dispatchers.Default) {}
    
  6. Coroutine Context

    每一个协程作用域CoroutineScope都需要一个上下文,协程上下文是Element实例的索引集,其中一个Element实例就是我们需要执行协程的工作线程,称之为Dispatchers调度器

    如果你使用默认调度器定义了父级协程作用域,那所有子协程默认都会继承这个上下文(调度器),除非你在launch函数或者withContext函数传入新的上下文参数,这也是你切线程的方式

    回到上面的例子WelcomingScreen,我们需要在IO线程调用async,然后在主线程得到结果,我们可以这样做,先修改funA

    suspend fun funA(): String {
        println("start....")    
        val helloDeffered = async(Dispatchers.IO) { funB() }
        val worldDeffered = async(Dispatchers.IO) { funC() }    
        println("......end")   
        return helloDeffered.await() + " " + worldDeffered.await()}
    

    再修改activity,再IO线程执行funA,再UI线程打印返回结果

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)    setContentView(R.layout.activity_main)    
        launch {
            println("Launch Thread: ${Thread.currentThread().name}")        
            val string = funA()        
            println("Logging Thread: ${Thread.currentThread().name}")        withContext(Dispatchers.Main) {
                Toast.makeText(
                    this@WelcomeScreen, string,Toast.LENGTH_LONG).show()
            }
        }
    }
    

    得到了我们想要的结果,再IO线程执行复杂操作,再主线程处理执行结果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章