Kotlin协程大法
协程是什么
首先,我们来回忆一下什么是进程和线程。
-
什么是进程
进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间。
直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。 -
什么是线程
线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。
线程是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,因此比进程更加的轻量级。
但是线程不能独立执行,必须依附在进程之上。
有一句话总结的很好:
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
协程
类似于一个进程可以拥有多个线程,一个线程也可以拥有多个协程,一个进程也可以单独拥有多个协程。
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。协程必须依附在进程或线程之上。
本质上,协程是轻量级的线程。
有了线程为什么还需要协程
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进阶技术请扫码关注公众号
微信扫码关注