如图,使用IDEA作为我们的IDE,用Gradle做依赖管理工具,以便后续引入依赖。JDK版本自由选择,我这里是JDK11.
新建工程后等待gradle构建完成,往build.gradle文件中添加协程依赖,如下代码:
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.0'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
}
同步一下就可以愉快开始玩耍啦。
协程在功能上是为了更简单、方便地处理异步逻辑。记住这个功能很重要,后续可以帮助我们理解和使用协程的实战场景。
根据传统习惯,先跑个HelloWorld吧。
fun main() = runBlocking {
launch {
delay(1000)
println("Coroutines")
}
println("Hello")
}
输出结果是
Hello
Coroutines
下面来解释里面的具体逻辑。
先讲launch
这个东西,看起来像关键字,实际上它是一个方法,接收一个高阶函数作为参数,以lambda的形式简化。如果你熟悉Kotlin的thread应用,那么就可以理解为launch像thread一样,thread{}
开了一个线程,而launch{}
开了一个协程。具体的任务都在代码块里。当然两者是有很大区别的,具体会后面提到。
言归正传,我们再看一下delay
这个函数。在IDE中看到它有一个特殊标记,这表明它是一个挂起函数,什么是挂起函数呢?简单来说就是函数有suspend修饰,后面会再详细提到。
这里可以把delay理解为Thread中的sleep方法,对协程进行休眠。
最后看看runBlocking
这个函数。如果把这个函数去掉了会怎么样呢?
launch就会提示找不到对应方法了。实际上,查看lauch函数的源码可知,这是一个CoroutineScope对象的扩展函数。而runBlocking方法在代码块内提供了这样一个CoroutineScope对象,launch的实际写法是this.launch{}
,这样就不难理解了。
一定要有这样一个CoroutineScope对象,我们利用这个对象的launch方法才能启动一个协程任务。
这个CoroutineScope对象的获取是多种多样的,这里用的是其中一种runBlocking方法获取,这个方法可以保证里面的协程任务都运行完了才结束该方法,避免出现主线程跑完了,协程任务还在阻塞,输出了结果也看不到的现象。
既然上面提到了协程作用域,这里就要展开讲讲了。这是kotlin协程的一个重要概念。Kotlin协程的核心思想是结构化并发。说人话,就是用一个个Scope把这些协程任务统一管理起来。
以前用线程来执行异步任务时,我们需要自己手动控制线程的wait和notify,手动控制线程的取消,这样的来来回回的工作一多就容易出错,异步编程的噩梦由来已久。Kotlin协程就不一样了,他说开发者你自己弄来弄去吃力不讨好,干脆我做一个协调线程的包,怎么做你不用怎么管,做一些调用工作就好了。那当然好啊。但是Kotlin协程具体怎么做呢?实际上就是把一个个协程任务,都让一个小组长来管理,也就是Scope。做协程的任务时有个人替我们看着了,自然就省心了。我们要做的就是管理一下这些包工头(创建、销毁Scope),发点任务下去(编写具体协程逻辑),至于包工头怎么下发,怎么命令下属,怎么调配人力,至少目前我们不关心~
下面我们看看suspend的简单用法。
fun main() = runBlocking {
launch {
delay(1000)
println("Coroutines")
}
println("Hello")
}
前面提到,launch
用来启动一个协程任务。那么随着任务越来越复杂,代码量增多,我们就开始考虑抽成一个函数,写出以下代码。
发现delay方法提示报错了,为什么呢?这里就是前面提到的:delay()
是一个挂起函数。挂起函数的使用要满足以下两个规则的其中一个:
知道了以上原则,很简单,我们让其满足第二条规则,在fun heavyTask()
前加上suspend关键字,就不会报错了。
suspend fun heavyTask(){
delay(1000)
println("Coroutines")
// 一百行代码
}
当然我们也可以这样写,在普通函数里新创建一个作用域来调挂起函数:
fun heavyTask() {
runBlocking {
delay(1000)
println("Coroutines")
// 一百行代码
}
}
只不过这种做法通过runBlocking新增了一个包工头(Scope)对象,增加了开销而已,语法上是没问题的。
前面提到,协程作用域创建是多种多样的,就像同是包工头,你可以通过社招,通过校招,通过亲戚介绍等等途径获得。每个包工头的作用也不一样,有的喜欢让组员五点下班,有的喜欢晚上干活。
前面提到可以用runBlocking获取Scope对象,当然也可以通过另外一个方法,这里介绍另外一个包工头coroutineScope函数。他们的特点都是会等到内部的协程任务执行完成后才会退出函数,区别就在于runBlocking会创建一个循环队列来保证不被中断,这个写过QT或者Java桌面应用或者了解Android底层的同学应该会很熟悉。而coroutineScope是一个挂起函数,是可以挂起的。
看了半天还是不懂?没关系,说实话,这两个模式的区别很难用代码举例,因为在runBlocking里面同时使用launch和coroutineScope,执行顺序是难以预测的。网上有一些例子,我看了半天也没弄懂,总觉得和官方的表述有出入。
这里给出几条关于使用上的总结:
没有分清也问题不大,不要被这两个差异阻挠了后面协程的学习哈。
下面做个小判断,猜一下这段代码输出结果是什么?
fun main() = runBlocking {
doWorld()
println("Done")
}
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
输出结果:
Hello
World 1
World 2
Done
runBlocking内没有显式地launch任务,而是使用了一个包工头(coroutineScope),在包工头内部启动了任务。
实际执行就是两个任务启动,然后被delay了,然后就去打印了“Hello”,等到短的时间结束后就打印“World 1”,再等到长的delay结束后就打印“World 2”。都打印完后,该包工头内的任务就做完了,接了了coroutineScope,也就是结束了doWorld()
函数,再回到下一步,打印“Done”。
前面一直讲,launch就是分配一个任务,这可不是为了形象才这样讲,而是launch确实会返回一个Job对象。
fun main() = runBlocking {
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
}
调用这个job对象的join方法,就会等待到该任务结束再执行下面的代码。通过这个方法可以显式地控制该任务的执行时机。
协程是轻量化的,这也是它与线程最大的不同。前面为了理解,说可以把协程暂时理解为加强版的线程。这部分就来看一下两者的不一样。
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
fun OOMMain() {
repeat(100_000) { // launch a lot of coroutines
thread {
Thread.sleep(5000L)
print(".")
}
}
}
上下两段代码跑一下应该就能看到两者的差异了。协程不会崩,线程这段会出现OOM。