美文网首页
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