协程是什么
首先,我们来回忆一下什么是进程和线程。
-
什么是进程进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间。直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。
-
什么是线程线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,因此比进程更加的轻量级。但是线程不能独立执行,必须依附在进程之上。有一句话总结的很好:
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。 -
什么是协程类似于一个进程可以拥有多个线程,一个线程也可以拥有多个协程,一个进程也可以单独拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程必须依附在进程或线程之上。本质上,协程是轻量级的线程。
有了线程为什么还需要协程
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. 子协程抛出未捕获的异常时,默认情况下会取消其父协程.
思考
协程的效率真的比线程效率高吗?如果不是,那什么情况下协程效率高,什么情况下线程效率高?实际线程的执行效率是远高于协程的,但是这要在避免频繁切换线程或者同步锁的情况下。欢迎大家勘误