Kotlin协程大法

335 阅读12分钟
原文链接: mp.weixin.qq.com

协程是什么

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

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

  2. 什么是线程线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,因此比进程更加的轻量级。但是线程不能独立执行,必须依附在进程之上。有一句话总结的很好: 对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。

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

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

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. 子协程抛出未捕获的异常时,默认情况下会取消其父协程.

思考

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