美文网首页
10.协程(一)

10.协程(一)

作者: ggghhhhhhhh | 来源:发表于2020-06-10 09:20 被阅读0次

    1.引

    在Android开发中,我们经常遇到的一个异步任务场景是:在后台执行一个复杂任务,下一个任务依赖于上一个任务的执行结果,所以必须等上一个任务完成后才能开始执行,具体的例子比如,当我们上传资源到服务器的时候,我们首先获取一个服务器的token,然后再通过这个token作为上传资源的校验,上次成功后再通知主线程更新ui

    private fun requestToken() : String{
        ...
    }
    
    private fun requestPost(String token):String{
        ...
    }
    
    private fun updateUI(String post){
        ...
    }
    

    以上三个函数中,前两个函数是耗时函数,不能运行在主线程,而第三个函数是更新ui操作,需要运行在主线程中,后两个函数都需要依赖上一个函数的返回结果,三个任务不能并行运行,那么该如何解决这个问题呢?

    1.1 回调

    对于这样的问题,我们常见的做法是,先执行第一个任务,执行完之后用回调通知,然后执行第二个任务,以此类推,最后通过handler通知主线程更新ui

    //开启一个线程
    thread {
                //执行第一个函数
                requestTokenAsync{ token->
                      requestPostAsync{ post->//执行第二个函数
                          handler.post{
                                updateUI(post);//执行第三个函数
                          }
                      }
                }
        }
    

    目前的大多数网络请求框架的做法都是使用这个样的回调方法,但随着任务数的增多,嵌套数会越来越多,使得程序变得非常难看,而且不方便处理异常。

    1.2 RxJava

    这种方法我们也可以使用RxJava的链式调用,这也是目前大多数人的选择

    Single.fromCallable { requestToken()) }//执行第一个函数
                    .map { token -> requestPost(token) }//执行第二个函数
                    .subscribeOn(Schedulers.io())//线程切换
                    .observeOn(AndroidSchedulers.mainThread())//切换到主线程
                    .subscribe({ post ->
                        updateUI(post)//执行第三个函数
                    }, { e ->
                        e.printStackTrace()
                    })
    

    RxJava是目前非常流行的异步处理框架,有丰富的操作符,简单的线程调度,异常处理等等,可以说满足大多数人的需求,是一个非常优秀而且强大的开源库,那么有没有更加简便的方法呢。

    1.3 协程

    用使用协程的代码

    private suspend fun requestToken() : String{ ... } //挂起函数
    private suspend fun requestPost(String token):String { ... } //挂起函数
    private fun updateUI(String post) { ...} 
    GlobalScope.launch(Dispatchers.Main) {
                val token = requestToken();
                val post = requestPost(token);
                updateUI(post)
            }
    

    可以看到,使用协程实现的代码非常简洁,以顺序的方式书写异步代码,不会柱塞当前的UI线程,错误处理也和平常的代码一样简单。

    2. 协程

    2.1 协程引入

    dependencies{
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
    }
    

    2.2 协程的定义

    官方中文文档:kotlin 中文文档
    什么是协程呢,我们先看官方的说法

    协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

    协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

    漫画版概念解释:漫画:什么是协程?

    说实话,但看文字有点难以理解
    而通常协程会跟线程(thread)进行比较,我们通过一张图开更加直观地跟线程对比


    看到这里,依然觉得云里雾里,不知所云,没有说到协程的本质,于是又去翻了翻官方文档。

    2.3协程的本质

    import kotlinx.coroutines.*
    fun main() = runBlocking {
        repeat(100_000){  //启动10万个协程
            launch {
                delay(1000L)
                print(".")
            }
        }
    }
    

    这个是官方例子,来说明协程优势的地方,如果我们用java的线程来表示

    repeat(100_000){
        thread{
           Thread.sleep(1000L)
           print(""."")
        }
    }
    

    这都不需要运行,我们都知道会发生什么。
    其实,这样对比有点不厚道,一个封装后的产物,跟原始线程比,本来就没什么可以比性,
    如果要比的话,也是要跟java的Executor比:

    repeat(100_000) {
        val executor = Executors.newSingleThreadScheduledExecutor()
        val task = Runnable {
            print(".")
        }
        repeat(100_00) {
            executor.schedule(task, 1, TimeUnit.SECONDS)
        }
    }
    

    用上面那段代码跑了下,跟上面协程的例子,并没有发现实质上的性能提升。所以到目前为止,我们也下一个结论
    使用Kotlin协程,在性能上并没有比我们原先的开发模式在性能上有多大的提升,因为我们多使用的各种线程切换库比如okhttp,AsyncTask等内部都实现了线程池,而不是直接使用Thread。
    但是上面并没有直接证实kotlin协程是一个基于java Thread封装的一个工具包,下面我们就来通过代码验证一下

    fun main(){
        //在没有开启协程前,先打印一下进程名称和进程id
        println(
            "Main: " +
                    "threadName = " + Thread.currentThread().name
                    + " threadId = " + Thread.currentThread().id
        )
    
        //循环20次
        repeat(20) {
            GlobalScope.launch {
                //开启协程后,先打印一下进程名称和进程id
                println(
                    "IO: " +
                            "threadName = " + Thread.currentThread().name
                            + " threadId = " + Thread.currentThread().id
                )
                delay(1000L)
            }
        }
    
    }
    
    打印
    看看打印发现了什么?开启的携程运行在不同的线程上,而且有一些线程的名字一模一样,是不是觉得跟java的线程池很像。
    所以到这里,我们不难得出,kotlin协程,不是真正意义上的协程(跟其他语言比如Go的协程就是真正意义上的协程),没有太神秘的地方,本质就是基于java Thread的封装,跟线程池的性质是一样的。
    但是既然跟线程池是一样的,我们为什么要学习线程呢,线程的好处在哪里,下面我们就来使用看看。

    3 协程的使用

    3.1协程的创建

    kotlin里没有new,自然也不像java李一样new Thread,而是通过一些专供函数来创建,比如kotlin里的协程就使用GlobleScope类创建,GlobleScope提供的几个构造函数:

    • launch -创建协程
    • async -创建带返回值的协程,返回的是Deferred类
    • withContext -不创建新的协程,而是指定协程上运行的代码块,指定线程
    • runBlocking -不是GlobalScope的api,可以独立使用,区别是runBlocking里面的delay会阻塞线程,而launch创建的不会

    kotlin在1.3之后要求协程必须由CoroutineScope创建,CoroutineScope不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。
    创建同一个协程

    GlobalScope.launch(Dispatchers.Default) {
                println("协程开始")
                val token = requestToken();
                val post = requestPost(token);
                updateUI(post)
                println("协程结束")
            }
    

    3.2 挂起函数(supend)

    协程里可以执行普通的函数,也可以执行挂起函数

    private suspend fun requestToken(): String {
            return withContext(Dispatchers.IO) {
                Thread.sleep(500);
                return@withContext "token"
            }
        }
    
    private suspend fun requestPost(): String {
            return withContext(Dispatchers.IO) {
                Thread.sleep(500);
                return@withContext "post"
            }
        }
    

    可以看到上面两个函数都是被suspend修饰,并且里面有调用withContext指定了线程调度器,像这样的被suspend修饰的函数,我们通常叫它为挂起函数。挂起函数处理被suspend修饰,跟普通的函数没有其他区别
    让协程执行一个挂起函数的时候,协程就会被挂起,等到挂起函数执行完之后才能接着下一步执行,在这个过程中,不会阻塞线程。要启动一个协程,至少有一个挂起函数,suspend修饰符通常可以标记函数、扩展函数和lambda表达式。

    //协程
    GlobalScope.launch(Dispatchers.Default) {
                println("协程开始")
                val token = requestToken(); //挂起函数
                val post = requestPost(token); //挂起函数
                updateUI(post)
                //处理异常  
                println("协程结束")
            }
    

    3.3 参数解析

    launch构造函数的接收了3个参数

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job
    
    • CoroutineContext 协程上下文,是一些元素的集合,主要包括了Job和CoroutineDispatcher元素,可以代表一个协程的场景
      EmptyCoroutineContext表示一个空的协程上下文
      CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池,通常用来指定协程运行的线程。kotlin提供了几种标准实现

      1. Dispatchers.Default 默认
      2. Dispatchers.Main 主线程
      3. Dispatchers.IO io线程
      4. Dispatchers.Unconfined 不执行线程
    • CoroutineStart 协程的启动模式

      模式 功能
      DEFAULT 立即执行协程体
      ATOMIC 立即执行协程体,但在开始运行之前不可取消
      UNDISPATCHED 立即在当前线程执行协程体,直到第一个suspend执行
      LAZY 只有在需要的时候执行,相当于懒加载
    • CoroutineScope.() -> Unit 最后一个参数,就是执行体了,相当于Thread.run,可以看到的是同样是使用suspend修饰的, 说明执行体本身就是一个挂起函数。

    启动模式CoroutineStart,我们通常用的最多的是DEFAULT ,LAZY

    //default没什么好说的,默认模式就是这个
    //lazy的用法
        val job:Job=GlobalScope.launch(context = Dispatchers.Default,start=CoroutineStart.LAZY) {
            println("协程开始时间:${System.currentTimeMillis()}")
        }
        println("主线程:${System.currentTimeMillis()}")
        Thread.sleep(1000)
        job.start()
    
    //输出
    主线程:1589253373955
    协程开始时间:1589253375019
    

    可以看到协程执行时间比在主线程晚了1s,这就是懒加载的作用

    3.4 withContext

    withContext{} 不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成
    简单点来说就是,给协程指定一个线程或者线程池,让协程在线程上面执行,知道执行结束。

    3.5 async

    CoroutineScope.async{}可以实现与launch 一样的效果,在后台创建一个新协程,唯一的区别是它是有返回值的,返回值类型是Deferred。

    scope.launch(Dispatchers.Main) {
                //async 相当于创建了一个异步任务,但是在这里还没开始执行
                //需要调用await()方法才会执行这个任务
                val one=async { api.listRepos1("lgh001") } //耗时任务
                val two=async { api.listRepos1("lgh001") } //耗时任务
                //await是一个suspend挂起函数,所以不需要使用withContext
                val same=one.await()[0]==two.await()[0]
                println(same)
            }
    

    上面代码 one.await()[0]==two.await()[0],直接比较,其实是因为await是一个挂起函数,协程执行挂起函数是顺序执行的,先执行one.await得到返回之后,再执行two.await,看起来像是普通的函数执行,比较。以同步的写法写异步的执行,说实话有点爽。

    3.6 协程的释放

    跟线程一样,如果当页面关闭的时候,协程还在执行耗时任务而不释放,就会导致内存泄漏的问题,解决方法也很简单,在关闭页面的时候释放即可

    GlobalScope.launch(Dispatchers.Main) {
          ...
    }
    //使用GlobalScope创建的协程,最后调用释放即可
     GlobalScope.cancel()
    
    //当然,在开启多个协程的时候,可以用这种方式,相当于一个集合,把所有的协程都装进来,最后统一全部释放
    val scope= MainScope()//创建一个scope
    scope.launch(Dispatchers.Main) {
          ...
    }
    scope.cancel()//最后释放,
    

    当然也可以使用lifecycle的方式释放,
    首先需要引用

    dependencies {
        implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
    }
    

    把需要的库引用进来之后,会提供一个扩展函数 lifecycleScope,直接用这个scope来开启协程,就不需要我们手动去释放协程了,原理就是监听lifecycle,统一释放

    lifecycleScope.launch(Dispatchers.Main) {
          ...
    }
    

    4.协程的优势

    在我们日常开发中,有一个场景是,在同一个页面中,需要请求两个api,而这两个api没有强关联,请求完成后需要进行合并,如果我们使用经典的回调方式:

            //创建两个请求
            val observable1 = Observable.just("1")
            val observable2 = Observable.just("2")
            //使用zip操作符合并两个请求
            Observable.zip<String, String, String>(observable1, observable2,
                io.reactivex.functions.BiFunction { t1, t2 ->
                    t1 + t2
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : Observer<String> {
                    override fun onComplete() {}
    
                    override fun onSubscribe(d: Disposable) {}
    
                    override fun onNext(t: String) {
                        println(t)
                    }
    
                    override fun onError(e: Throwable) {}
    
                })
    

    上面代码即使使用了rxjava,依然会觉得非常麻烦,如果是更加复杂的需求,可能会需要使用更加复杂的操作符,或者多个操作符相互操作才能达到效果
    但是,如果使用kotlin协程

    GlobalScope.launch {
                //使用async发起两个异步请求
                val res1=async { reqeust1() }
                val res2=async { reqeust1() }
                //得到结果之后合并
                val res3=res1.await()+res2.await()
                
                println(res3)
            }
    

    看到这里,我们再来看看协程这个名字,英文名叫Coroutine,中文全称叫"协同程序",是不是对协程有了全新的理解

    5.总结

    1.协程就是对java Thread的封装,可以帮我们写出更加复杂的并发代码
    2.协程依赖于线程而存在,协程必须运行在线程上,一个线程可以有多个协程,协程也可以运行在不同的线程中
    3.kotlin协程可以极大地简化异步编程,以顺序执行的书写方式,写异步执行的代码。

    相关文章

      网友评论

          本文标题:10.协程(一)

          本文链接:https://www.haomeiwen.com/subject/laqwghtx.html