美文网首页
Kotlin协程大法

Kotlin协程大法

作者: FlyerGo | 来源:发表于2020-02-26 11:29 被阅读0次

    协程是什么

    首先,我们来回忆一下什么是进程和线程。

    • 什么是进程
      进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间。
      直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。

    • 什么是线程
      线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。
      线程是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,因此比进程更加的轻量级。
      但是线程不能独立执行,必须依附在进程之上。

    有一句话总结的很好:

    对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。

    协程

    类似于一个进程可以拥有多个线程,一个线程也可以拥有多个协程,一个进程也可以单独拥有多个协程。
    协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程必须依附在进程或线程之上。

    本质上,协程是轻量级的线程。

    有了线程为什么还需要协程

    A. 举一个简单的消费者和生产者的例子。

    public class Test {
    
        public static void main(String[] args){
    
            Queue<Integer> workQueue = new LinkedList<>();
            Thread producerThread = new Thread(new Producer(workQueue));
            Thread consumerThread = new Thread(new Consumer(workQueue));
    
            producerThread.start();
            consumerThread.start();
        }
    
        //生产者线程
        public static class Producer implements Runnable {
            private Queue<Integer> workQueue;
    
            private static final int MAX_WORKER = 10;
    
            public Producer(Queue<Integer> workQueue) {
                this.workQueue = workQueue;
            }
    
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    synchronized (workQueue){
                        while (workQueue.size() >= MAX_WORKER){
                            System.out.println("队列满了,等待消费");
                            try {
                                workQueue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        workQueue.add(i);
                        System.out.println("完成一个生产任务");
                        workQueue.notify();
                    }
                }
            }
        }
    
        //消费者线程
        public static class Consumer implements Runnable {
            private Queue<Integer> workQueue;
    
            private static final int MAX_WORKER = 10;
    
            public Consumer(Queue<Integer> workQueue) {
                this.workQueue = workQueue;
            }
    
            @Override
            public void run() {
    
                while (true){
    
                    synchronized (workQueue){
                        while (workQueue.size() == MAX_WORKER){
                            System.out.println("队列空了,等待生产");
                            try {
                                workQueue.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        int work = workQueue.poll();
                        System.out.println("消费了一个任务:" + work);
                        workQueue.notify();
                    }
                }
            }
        }
    
    }
    

    上面的代码简单地模拟了生产者/消费者模式,但是却并不是一个高性能的实现。
    为什么性能不高呢?原因如下:
    a. 涉及到同步锁。
    b. 涉及到线程阻塞状态和可运行状态之间的切换。
    c. 实际开发中可能还会涉及到线程上下文的切换。
    d. ……..

    以上涉及到的任何一点,都是非常耗费性能的操作
    如果使用协程是怎么样的情况呢?看代码

    import kotlinx.coroutines.*
    import kotlinx.coroutines.channels.*
    
    fun main() = runBlocking {
        val numbers = produceNumbers() // 开始生产
        val squares = square(numbers) // 开始消费
    }
    
    fun CoroutineScope.produceNumbers() = produce<Int> {
        var x = 1
        while (true) send(x++) // 从 1 开始的无限的整数流
    }
    
    fun CoroutineScope.square(numbers: ReceiveChannel<Int>): ReceiveChannel<Int> = produce {
        for (x in numbers) send(x * x)
    }
    
    

    我的天啊,怎么可能这么简单 这是蒙我的吧??

    还有一个很典型的例子就是:

    同时启动10万个协程和10条线程去做同样的事情,会有什么样的结果?
    结果就是:协程能顺利执行完任务,线程却有可能会报内存不足的错误。

    上面说到协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
    这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
    因而使用协程能给你的程序代理更高、更稳定的性能表现。

    B. 使用协程能够避免回调地狱
    举一个登录的例子:账号密码登录 -> 获取用户信息 -> 获取用户好友列表 -> 跳转到特定界面。
    我们来写一下它的伪代码大概是这样的:

    logig(new CallBack() {
          @Override
         void success() {
           getUserinfo(new CallBack() {
              @Override
              void success() {
                  getFrendsList(new CallBack() {
                     @Override
                     void success() {
                     //切换到线程跳转界面
                               }
                       });
                    }
              });
        }
    });
    
    

    看到这种嵌套式的回调就想吐有木有?
    如果使用协程呢?它的伪代码是这样子的

    coroutineScope.launch(Dispatchers.Main) {             // 👈 在 UI 线程开始
        val friendList = withContext(Dispatchers.IO) {  // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程
           login()
           getUserinfo()
           getFrendsList()  // 👈 将会运行在 IO 线程
        }
        startFriendListActivity() // 👈 回到 UI 线程更新 UI
    }
    
    

    是不是感觉好多了,给人一种顺序执行的感觉。
    当然你说RxJava也可以达到这样的效果啊…..(我们来偷偷删掉它👈)

    协程如何使用

    首先要导入依赖库

    //Android 工程使用
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
    //Java 工程使用
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x'
    
    

    如何使用协程?直接上注释代码:

    
    // 方法一,使用 runBlocking 顶层函数
    // 该方法是线程阻塞的
    runBlocking {
        login()
    }
    
    // 方法二,使用 GlobalScope 单例对象
    //该方法不是线程阻塞的,但是生命周期和APP的生命周期一样,而且不能取消
    GlobalScope.launch {
        login()
    }
    
    // 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
    //注意这里的context并非Android中的上下文context
    //context是CoroutineContext类型参数, 可以通过CoroutineContext去管理和控制协程的生命周期
    //在实际开发中一般推荐使用这种方法使用协程
    val coroutineScope = CoroutineScope(context)
    coroutineScope.launch {
        login()
    }
    
    

    具体使用区别看注释!!!

    协程如何切换线程?

    我们看看launch方法

    
    /**
    * 第一个参数是不仅可以用来协程之间传递参数,还可以制定协程的执行线程
    * 第二个参数很少用,除非你需要手动启动协程,一般协程创建即启动
    */
    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        val newContext = newCoroutineContext(context)
        val coroutine = if (start.isLazy)
            LazyStandaloneCoroutine(newContext, block) else
            StandaloneCoroutine(newContext, active = true)
        coroutine.start(start, coroutine, block)
        return coroutine
    }
    
    

    我们可以使用Dispatchers.IO参数把任务切到 IO 线程,
    使用Dispatchers.Main参数把任务切换到主线程,
    或者使用Dispatchers.Unconfined制定在当前线程开启协程。

    //在IO线程开启协程
    coroutineScope.launch(Dispatchers.IO) {
        ...
    }
    
    //在主线程开启协程
    coroutineScope.launch(Dispatchers.Main) {
        ...
    }
    协程的线程切换:
    
    coroutineScope.launch(Dispatchers.Main) {
        //            👇  async 函数之后再讲
        val user = async {
               api.login()
           }    // 子线程获取数据
        // 祝线程更新 UI
    }
    
    

    或者可以这样:

    coroutineScope.launch(Dispatchers.IO) {
        // IO线程开启协程,获取数据
        val user = login()
        launch(Dispatch.Main) {
            //在主线程更新UI
        }
    }
    
    

    或者使用withContext控制切换:

    
    coroutineScope.launch(Dispatchers.Main) {
        val token = withContext(Dispatchers.IO) { // 切换到IO线程
            login()
        }
        // 回到 UI 线程更新 UI
    }
    
    

    通过使用withContext可以大大减少嵌套:

    
    coroutineScope.launch(Dispachers.Main) {
        ...
        withContext(Dispachers.IO) {
            ...
        }
        ...
        withContext(Dispachers.IO) {
            ...
        }
        ...
    }
    
    

    协程的挂起

    使用suspend 关键字。
    例如我们登陆成功后再获取用户信息的例子:

    
    suspend fun login(): Token {
         // 登陆并返回Token
    }
    
    suspend fun getUserInfo(val toekn:Token): User {
        //获取用户信息
    }
    
    coroutineScope.launch(Dispatchers.Main) {
        val user = withContext(Dispatchers.IO) { // 切换到IO线程
                val toekn = login()
                //这里如果login没有执行完是不会执行getUserInfo的
                getUserInfo(toekn)
        }
        // 回到 UI 线程更新 UI
    }
    
    

    协程的取消

    在创建协程过后可以接受一个 Job 类型的返回值,我们操作 job 可以取消协程任务,job.cancel方法就可以取消协程了。
    需要注意的是协程的取消有些特质,因为协程内部可以在创建协程的,这样的协程组织关系可以称为父协程,子协程:
    a. 父协程手动调用 cancel() 或者异常结束,会立即取消它的所有子协程;
    b. 父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成;
    c. 子协程抛出未捕获的异常时,默认情况下会取消其父协程.

    思考

    协程的效率真的比线程效率高吗?如果不是,
    那什么情况下协程效率高,什么情况下线程效率高?
    实际线程的执行效率是远高于协程的,但是这要在避免频繁切换线程或者同步锁的情况下。
    欢迎大家勘误

    更多Android进阶技术请扫码关注公众号


    微信扫码关注

    相关文章

      网友评论

          本文标题:Kotlin协程大法

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