Kotlin协程是个什么东西?

协程是什么

根据维基百科的定义,协程(Coroutine)是计算机程序的一类组件,推广了协做式多任务的子程序,容许执行被挂起与被恢复。android

协程(Coroutine)并非一个新词,马尔文·康威于1958年发明了术语“coroutine”,并将它用于汇编程序。而在其余语言,如Go、Python也都有协程的概念,因此它也不是Kotlin独有的。git

在不一样的语言层面上,协程的实现方式是不太同样的,本文介绍的Kotlin协程在本质上,它是一种轻量级的线程github

Kotlin协程是运行在线程中的,这里的线程能够是单线程,也能够是多线程。在单线程使用协程,比不使用协程的耗时并不会少。编程

上面介绍的都是协程的一些概念,以及Kotlin协程的特色。那究竟为何会有Kotlin协程?它究竟比线程好在哪里?咱们继续往下看。promise

Kotlin协程初认识

在Kotlin中,协程就是线程的封装,它提供了一套标准的API来帮助咱们写并发任务。回想一下,在Java和Android中,咱们是怎么写并发任务的?markdown

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.launchCoroutineScope.async能够开启协程,经过CoroutineContext也能够控制协程的生命周期。在开发过程当中,通常推荐使用这种方式开启协程。

CoroutineContext

上面说到推荐使用CoroutineScope.launch开启协程,而不论是GlobalScope.launch仍是CoroutineScope.launchlaunch方法的第一个参数就是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}")
}
复制代码

程序运行结果以下:

image.png

能够看到,Dispatchers.Main调度器的协程运行在主线程,而无调度器、Dispatchers.IODispatchers.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中运行上述代码,执行结果是:

image.png

经过打印的日志能够看到,虽然协程的代码顺序在println("end test fun...")以前,可是在执行顺序上,协程的启动仍然在println("end test fun...")以后,结合非阻塞式挂起,下图展现了协程的执行顺序流程:

image.png

参考文档

一、Kotlin 的协程用力瞥一眼 - 学不会协程?极可能由于你看过的教程都是错的

二、协程入门指南

三、最全面的kotlin协程

相关文章
相关标签/搜索