Kotlin协程大法

2020-02-26  本文已影响0人  FlyerGo

协程是什么

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

有一句话总结的很好:

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

协程

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

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

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

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进阶技术请扫码关注公众号


微信扫码关注
上一篇下一篇

猜你喜欢

热点阅读