Kotlin 协程是个什么东西?
↓推荐关注↓
协程是什么
根据维基百科的定义,协程(Coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。
协程(Coroutine)并不是一个新词,马尔文·康威于1958年发明了术语“coroutine”,并将它用于汇编程序。而在其他语言,如Go、Python也都有协程的概念,所以它也不是Kotlin独有的。
在不同的语言层面上,协程的实现方式是不太一样的,本文介绍的Kotlin协程在本质上,它是一种轻量级的线程。
Kotlin协程是运行在线程中的,这里的线程可以是单线程,也可以是多线程。在单线程使用协程,比不使用协程的耗时并不会少。
上面介绍的都是协程的一些概念,以及Kotlin协程的特点。那究竟为什么会有Kotlin协程?它究竟比线程好在哪里?我们继续往下看。
Kotlin协程初认识
在Kotlin中,协程就是线程的封装,它提供了一套标准的API来帮助我们写并发任务。回想一下,在Java和Android中,我们是怎么写并发任务的?
Java实现多任务并发
在Java中,我们可以使用线程或者线程池来实现多任务并发:
//线程
new Thread(new Runnable() {
@Override
public void run() {
//耗时的工作
}
}).start();
//线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new Runnable() {
@Override
public void run() {
//耗时的工作
}
});
Android实现多任务并发
在Android中,除了可以通过Java的方式,创建线程、使用线程池实现多任务并发之外,还可以AsyncTask
等方式来实现多个耗时任务的并发执行:
//AsyncTask
public abstract class AsyncTask<Params, Progress, Result> {
//线程池中执行,执行耗时任务
protected abstract Result doInBackground(Params... params);
//UI线程中执行,后台任务进度有变化则执行该方法
protected void onProgressUpdate(Progress... values) {}
//UI线程执行,耗时任务执行完成后,该方法会被调用,result是任务的返回值
protected void onPostExecute(Result result) {}
}
无论是Java还是Android提供的组件,都可以实现多任务并发的执行,但是上面的组件都或多或少存在着问题:
耗时任务执行结束后,子线程要将结果传递回主线程,两者之间的通信不太方便。 AsyncTask
处理的回调方法比较多,当有多个任务时可能会出现回调嵌套。
协程实现多任务并发
继续以AsyncTask
举个🌰:
AsyncTask<String, Integer, String> task = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String... strings) {
String userId = getUserId(); //获取userId
return userId;
}
@Override
protected void onPostExecute(final String userId) {
AsyncTask<String, Integer, String> task1 = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String... strings) {
String name = getUserName(userId); //获取userName,需要用到userId
return name;
}
@Override
protected void onPostExecute(String name) {
textView.setText(name); //设置到TextView控件中
}
};
task1.execute(); //假设task1是一个耗时任务,去获取userName
}
};
task.execute(); //假设task是一个耗时任务,去获取userId
如果是使用协程,上面的例子可以简化为:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserId() //耗时任务,这里会切换到子线程
val userName = getUserName(userId) //耗时任务,这里会切换到子线程
textView.text = userName //设置到TextView控件中,切换到主线程
}
suspend fun getUserId(): String = withContext(Dispatchers.IO) {
//耗时操作,返回userId
}
suspend fun getUserName(userId: String): String = withContext(Dispatchers.IO) {
//耗时操作,返回userName
}
上面launch
函数的{}的逻辑,就是一个协程。
相比于AsyncTask
的写法,使用kotlin协程有以下好处:
协程将耗时任务和UI更新放在了上下三行处理,消除了 AsyncTask
的回调嵌套,使用起来更加方便、简洁。协程通过挂起与恢复,将耗时任务的结果直接返回给调用方,使得主线程能直接使用子线程的结果,UI更新更加方便。
Kotlin协程的接入与使用
怎么接入
在模块的build.gradle
中加入以下依赖:
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
}
怎么使用
Kotlin提供了三种方式来创建协程,如下所示:
//方式一
runBlocking { //runBlocking是一个顶级函数
...
}
//方式二
GlobalScope.launch { //GlobalScope是一个单例对象,直接使用launch开启协程
...
}
//方式三
val coroutineScope = CoroutineScope(context) //使用CoroutineContext创建CoroutineScope对象,通过launch开启协程
coroutineScope.launch {
...
}
方式一:它是线程阻塞的,它通常被用在单元测试和main函数中,平时的开发中我们一般不会用到它。 方式二:与方式一相比,它不会阻塞线程,但是它的生命周期和应用是一致的,而且无法做到取消(后面会讲到),所以也不推荐使用。 方式三:通过 CoroutineContext
来创建一个CoroutineScope
对象,通过CoroutineScope.launch
或CoroutineScope.async
可以开启协程,通过CoroutineContext
也可以控制协程的生命周期。在开发过程中,一般推荐使用这种方式开启协程。
CoroutineContext
上面说到推荐使用CoroutineScope.launch
开启协程,而不管是GlobalScope.launch
还是CoroutineScope.launch
,launch
方法的第一个参数就是CoroutineContext
,源码如下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
这里的context
,即CoroutineContext
,它的其中一个作用是起到线程切换的功能,即协程体将运行在CoroutineContext
表征的指定的线程中。
Kotlin协程官方定义了几个值,可供我们在开发过程中使用,它们分别是:
Dispatchers.Main
协程体将运行在主线程,用于UI的更新等需要在主线程执行的场景,这个大家应该都清楚。
Dispatchers.IO
协程体将运行在IO线程,用于IO密集型操作,如网络请求、文件操作等场景。
Dispatchers.Default
协程体将运行在默认的线程,context没有指定或指定为Dispatchers.Default,都属于这种情况。用于CPU密集型,如涉及到大量计算等场景。要特别注意的是,这里的默认线程,其实和上面的IO线程共享同一个线程池。
Dispatchers.Unconfined
不受限的调度器,在开发中不应该使用它,暂不研究。
看一下下面这个例子:
GlobalScope.launch(Dispatchers.Main) {
println("Main Dispatcher, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch {
println("Default Dispatcher1, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.IO) {
println("IO Dispatcher, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Default) {
println("Default Dispatcher2, currentThread=${Thread.currentThread().name}")
}
程序运行结果如下:
可以看到,Dispatchers.Main
调度器的协程运行在主线程,而无调度器、Dispatchers.IO
、Dispatchers.Default
调度器的协程运行在同一个线程池。
launch与async
上面提到可以使用launch
来创建一个协程,但是除了使用launch
之外,Kotlin还提供了async
来帮助我们创建协程。两者的区别是:
launch:创建一个协程,返回一个 Job
,但是并不携带协程执行后的结果。async:创建一个协程,返回一个 Deferred
(也是一个Job),并携带协程执行后的结果。
async
返回的Deferred
是一个轻量级的非阻塞future,它代表的是一个将会在稍后提供结果的promise,所以它需要使用await
方法来得到最终结果。拿Kotlin官方的一个例子对async
进行说明:
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了些有用的事
return 29
}
执行上述代码,得到的结果是:
The answer is 42
Kotlin协程的使用场景
线程切换
在介绍《CoroutineContext》一节时,举的例子中的协程还是运行在单一线程中。在实际开发过程中,常见的场景就是线程的切换与恢复,这需要用到withContext
方法了。
withContext
我们继续以《与线程的对比》这一节的例子来说明:
GlobalScope.launch(Dispatchers.Main) {
val userId = withContext(Dispatchers.IO) {
getUserId() //耗时任务,这里会切换到子线程
}
textView.text = userId //设置到TextView控件中,切换到主线程
}
上面是一个典型的网络请求场景:一开始运行在主线程,然后需要到后台获取userId
的值(这里会执行getUserId
方法),获取结束,结果返回后,会切换回主线程,最后更新UI控件。
在获取userId
的时候,调用了getUserId
方法,这里用到了withContext
方法,将线程从main
切换到了IO
线程,当耗时任务执行结束后(即上面的getUserId
方法执行完毕),withContext
的另外一个作用是恢复到切换子线程前的所在线程,对应上面的例子是main
线程,所以我们才能做更新UI控件的操作。
我们也可以将withContext
的逻辑单独放到一个方法去管理,如下所示:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserIdAsync()
textView.text = userId //设置到TextView控件中,切换到主线程
}
fun getUserIdAsync() = withContext(Dispatchers.IO) {
getUserId() //耗时任务,这里会切换到子线程
}
这样看上去就像在使用同步调用的方式执行异步逻辑,但是如果按照上面的方式来写,IDE会报错的,提示信息是:Suspend function'withContext' should be called only from a coroutine or another suspend funcion
。
意思是withContext
是一个suspend
方法,它需要在协程或另外一个suspend
方法中被调用。
suspend
suspend
是Kotlin协程的一个关键字,它表示 “挂起” 的意思。所以上面的报错,只要加上suspend
关键字就能解决,即:
GlobalScope.launch(Dispatchers.Main) {
val userId = getUserIdAsync()
textView.text = userId //设置到TextView控件中,切换到主线程
}
suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
getUserId() //耗时任务,这里会切换到子线程
}
当代码执行到suspend
方法时,当前协程就会被挂起,这里所说的挂起是非阻塞的,也就是说它不会阻塞当前所在的线程。这就是所谓的“非阻塞式挂起”。
非阻塞式挂起与协程的执行步骤
非阻塞式挂起的一个前提是:涉及的必须是多线程的操作。因为阻塞的概念是针对单线程而言的。当我们切换了线程,那肯定是非阻塞的,因为耗时的操作跑到别的线程了,原来的线程就自由了,该干嘛干嘛呗~
如果在主线程中启动多个协程,那么协程的执行顺序是怎样的呢?是按照代码顺序执行么?还是有别的执行顺序?如下代码所示,假设test方法在主线程中执行,那么这段代码应该输出什么呢?
//假设test方法运行在主线程
fun test() {
println("start test fun, thread=${Thread.currentThread().name}")
//协程A
GlobalScope.launch(Dispatchers.Main) {
println("start coroutine1, thread=${Thread.currentThread().name}")
val userId = getUserIdAsync()
println("end coroutine1, thread=${Thread.currentThread().name}")
}
//协程B
GlobalScope.launch(Dispatchers.Main) {
println("start coroutine2, thread=${Thread.currentThread().name}")
delay(100)
println("end coroutine2, thread=${Thread.currentThread().name}")
}
println("end test fun, thread=${Thread.currentThread().name}")
}
suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
println("getUserIdAsync, thread=${Thread.currentThread().name}")
delay(1000)
return@withContext "userId from async"
}
在Android中运行上述代码,执行结果是:
通过打印的日志可以看到,虽然协程的代码顺序在println("end test fun...")
之前,但是在执行顺序上,协程的启动仍然在println("end test fun...")
之后,结合非阻塞式挂起,下图展示了协程的执行顺序流程:
参考文档
1、Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的
https://kaixue.io/kotlin-coroutines-1/
2、协程入门指南
https://ethanhua.github.io/2018/02/27/coroutines-guide_cn/
3、最全面的kotlin协程
https://juejin.cn/post/6844904037586829320
转自:掘金 AndroidHint
https://juejin.cn/post/6954393446622691342
- EOF -
看完本文有收获?请分享给更多人
推荐关注「安卓开发精选」,提升安卓开发技术
点赞和在看就是最大的支持❤️