本文为协程的开篇作,作者目前对协程的理解仍存在一些疑问,欢迎批评指正。
概念
⼀些 API 启动⻓时间运⾏的操作(例如⽹络 IO、⽂件 IO、CPU 或 GPU 密集型任务等),并要求调⽤者阻塞直到它们完成,通常的做法是使用异步加回调的方式来实现非阻塞,但异步回调代码写起来并不容易,尤其出现嵌套回调。
协程提供了⼀种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的⽅法:协程挂起。
kotlin协程是一种用户态的轻量级线程。
协程主要是让原来要使用"异步+回调方式"写出来复杂代码,简化成可以用看似同步的方式,这样我们就可以按串行的思维模式去组织原本分散在不同上下文的代码逻辑。
//伪代码
launch(Background) {
val bitmap = MediaStore.getBitmap(uri)
launch(UI) {
imageView.setImageBitmap(bitmap)
}
}
集成环境
- kotlin插件
ext.kotlin_version = '1.3.11'
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
- 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0"
//或使用android
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"
- experimental声明
//在module的build.gradle中声明
kotlin {
experimental {
coroutines 'enable'
}
}
官方文档
https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html
由于协程核心库由experimental转为release时间不长,第三方的blog几乎都是基于experimental的(API与最新版存在差异),目前来看官网的文档最是大而全,但也稍有滞后性。
最新版本的协程库可以去mavenCenter搜索。
启动协程的方法
通常启动协程有launch和async方法。
launch启动协程
@Test
fun coroutineDemo1(){
println("test func coroutineDemo1")
GlobalScope.launch {
delay(2000)
println("coroutine finish")
}
println("coroutine start")
Thread.sleep(3000)
println("coroutine end")
}
运行结果
I/System.out: coroutine start
I/System.out: coroutine finish
可以看到launch函数是以非阻塞的方式启动一个协程,而Thread.sleep是阻塞式的。
事实上launch方法有三个参数,并返回一个Job对象。
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
}
suspend函数
代码中的delay函数就是一个挂起函数,它用suspend关键字修饰,挂起函数只能从一个协程代码内部调用,普通代码不能调用,可以修改示例验证。
runBlocking函数
上面的例子中既有非阻塞的delay函数,又有Thread.sleep的阻塞函数,这样可读性太差,我们可用runBlocking来实现。
@Test
fun testRunBlocking() = runBlocking {
println("test func testRunBlocking")
GlobalScope.launch {
delay(2000)
println("coroutine finish")
}
println("coroutine start")
delay(3000)
}
注:如果不使用runBlocking那么我们是不能在函数体内调用delay函数的。
一般runBlocking函数不用来当做普通协程函数使用,它的主要目的是用来桥接普通阻塞代码和挂起风格的非阻塞代码,例如用在main函数或测试用例中。
协程的运行状态
Job状态 | isActive | isCompleted |
---|---|---|
new(可选的初始状态) | false | false |
active(默认的初始状态) | true | false |
completed(结束状态) | false | true |
当以默认参数创建协程时,协程就处于active状态,若想修改这种行为,可以使用Lazy模式创建
@Test
fun coroutineState() = runBlocking {
println("test func coroutineState")
val job = GlobalScope.launch {
delay(2000)
println("coroutine finish")
}
println("coroutineState 111 isActive:${job.isActive} isCompleted:${job.isCompleted} ")
delay(3000)
println("coroutineState 222 isActive:${job.isActive} isCompleted:${job.isCompleted} ")
}
等待协程执行完毕
默认情况下协程的跟随当前活动线程,如果协程中的挂起方法未执行完毕,而活动线程已经退出,则当前协程也就退出了,所以他更像一个守护线程。
@Test
fun coroutineJoinTest() = runBlocking {
println("test func coroutineJoinTest")
val job = GlobalScope.launch {
delay(2000)
println("coroutine finish thread:${Thread.currentThread()}")
}
job.join()
println("test func coroutineJoinTest end")
}
async启动协程
多个挂起函数的顺序执行和异步并发执行的效率问题,我们先来看顺序执行。
@Test
fun testSequential() = runBlocking {
println("[testSequential] start thread:${Thread.currentThread()}")
//统计时长
val time = measureTimeMillis {
val one = doJob1()
val two = doJob2()
println("[testSequential] result :${one + two}")
}
println("[testSequential] completed in :$time ms")
}
使用async函数实现异步并发执行,与launch函数不同的是启动async协程必须指定调度器。
@Test
fun testAsync() = runBlocking(Dispatchers.Default) {
println("[testAsync] start thread:${Thread.currentThread()}")
val time = measureTimeMillis {
val one = GlobalScope.async(Dispatchers.Unconfined) {
println("[doJob1] thread:${Thread.currentThread()}")
doJob1() }
val two = GlobalScope.async(Dispatchers.Main) {
println("[doJob2] thread:${Thread.currentThread()}")
doJob2() }
println("[testAsync] result :${one.await() + two.await()}")
}
println("[testAsync] completed in :$time ms")
}
async返回一个DeferredCoroutine对象,它是一种轻量级的非阻塞future,表示后面后提供结果。通过await函数获取结果,同时它与StandaloneCoroutine(launch函数返回的协程对象)一样都是AbstractCoroutine的子类型,因此也具有isActive和isCompleted属性。
取消协程
由launch函数启动的协程返回一个Job对象引用当前协程,可通过该对象的cancel函数取消正在运行的协程。
@Test
fun coroutineCancelTest() = runBlocking {
println("test func coroutineCancelTest")
val job = GlobalScope.launch {
repeat(100) { i ->
println("coroutine I' am sleeping i:$i")
delay(300)
}
delay(2000)
println("coroutine finish thread:${Thread.currentThread()}")
}
delay(2000)
job.cancel()
println("test func coroutineJoinTest end isActive:${job.isActive} isCompleted:${job.isCompleted}")
}
cancel可以取消一个正在运行的挂起函数,但是不能取消一个计算函数,此时可能需要判断协程状态。
@Test
fun coroutineCancelTest2() = runBlocking {
println("test func coroutineCancelTest2")
val job = GlobalScope.launch {
var i = 0
while(i < 100000) {
//用isActive检验协程的状态
if(!isActive) {
return@launch
}
if(i % 7 == 0) {
println("coroutine executing i:$i")
}
i++
}
println("coroutine finish thread:${Thread.currentThread()}")
}
delay(200)
job.cancel()
println("test func coroutineCancelTest2 end")
}
协程上下文和调度
- Dispatchers.Default 默认调度器(普通工作线程)
- Dispatchers.IO 普通IO线程
- Dispatchers.Main 安卓主线程
- Dispatchers.Unconfined
- newSingleThreadContext("myThread") 自定义线程
@Test
fun testDispatchers() = runBlocking {
println("[testDispatchers] start thread:${Thread.currentThread()}")
val jobs = arrayListOf()
jobs.add(GlobalScope.launch(Dispatchers.Unconfined) {
println("Unconfined is working thread:${Thread.currentThread()}")
})
jobs.add(GlobalScope.launch(Dispatchers.Main) {
println("Main is working thread:${Thread.currentThread()}")
doJob1()
})
jobs.add(GlobalScope.launch(Dispatchers.Default) {
println("Default is working thread:${Thread.currentThread()}")
doJob1()
})
jobs.add(GlobalScope.launch(newSingleThreadContext("myThread")) {
println("newSingleThreadContext is working thread:${Thread.currentThread()}")
})
jobs.forEach {
it.join()
}
println("testDispatchers....... end")
}
- withContext协程内部调度,切换线程
@Test
fun testWithContext() = runBlocking(Dispatchers.Main) {
println("[testWithContext] start")
val view = View()
val provider = TestDataProvider()
view.showLoading()
val result = withContext(Dispatchers.IO) {provider.loadData()}
view.showData(result)
}
协程的嵌套
当我们使用协程A的上下文启动另一个协程B时, B将成为A的子协程。当父协程A任务被取消时, B以及它的所有子协程都会被递归地取消。
@Test
fun testInnerCoroutines() = runBlocking {
println("[testInnerCoroutines] start ")
val request = GlobalScope.launch {
println("主协程 thread:${Thread.currentThread()}")
val job1 = GlobalScope.launch {
println("job1: 独立的协程上下文! thread:${Thread.currentThread()}")
delay(1000)
println("job1: 不会受到request.cancel()的影响")
}
// 继承父上下文
val job2 = GlobalScope.launch(Dispatchers.Unconfined) {
println("job2: 是request coroutine的子协程 thread:${Thread.currentThread()}")
delay(1000)
println("job2: 当request.cancel(),job2也会被取消")
}
job1.join()
job2.join()
}
delay(500)
request.cancel()
delay(1000)
println("main: Who has survived request cancellation?")
}
特点与优势
协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中⼀个会导致⼀些重要的任务被延迟。
另⼀方面,协程挂起几乎是无代价的。不需要上下文切换或者 OS 的任何其他干预(但基于现在的协程使用确实已切换线程)。最重要的是,挂起可以在很大程度上由用户控制:我们可以决定挂起时发生什么并根据需求优化/记日志等。
使用协程,我们不再需要像异步编程时写那么一堆callback函数,代码结构不再支离破碎,整个代码逻辑上看上去和同步代码没什么区别,简单,易理解,优雅。
基本原理
协程完全通过编译技术实现(不需要来自 VM 或 OS 端的支持),挂起机制是通过状态机来实现,其中的状态对应于挂起调用。
- 轻量级
@Test
fun testLightWeightCoroutine() = runBlocking {
println("[testLightWeightCoroutine] start")
val jobs = List(100000) {
GlobalScope.launch {
delay(1000L)
print(".")
}
}
jobs.forEach { it.join() } // wait for all jobs to complete
println("----")
println("[testLightWeightCoroutine] end")
}
而运行下面的代码就会直接OOM
@Test
fun testThread(){
val jobs = List(100000) {
Thread({
Thread.sleep(1000L)
print(".")
})
}
jobs.forEach { it.start() }
jobs.forEach { it.join() }
}
但是通过日志打印线程,发现协程使用的是线程池,这样比较是否合理?
协程拓展的演变领域
- kotlinx-coroutines-rx
- kotlinx-coroutines-android
- kotlinx-coroutines-swing
- kotlinx-coroutines-nio