kotlin coroutines 协程
Coroutine 协程,是kotlin 上的一个轻量级的线程库,对比 java 的 Executor,主要有以下特点:
- 更轻量级的 api 实现协程
- async 和 await 不作为标准库的一部分
- suspend 函数,也就是挂起函数是比 java future 和 promise 更安全并且更容易使用
那么实际本质上和线程池有什么区别呢?我的理解是这样的,协程是在用户态对线程进行管理的,不同于线程池,协程进一步管理了不同协程切换的上下文,协程间的通信,协程挂起,对于线程挂起,粒度更小,而且一般不会直接占用到CPU 资源,所以在编程发展的过程中,广义上可以认为 多进程->多线程->协程。
简单使用
首先,要引入 coroutines 的依赖,在你的 build.gradle
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0'
}
然后下面是一个最简单的例子,在子线程延迟打印一行日志:
fun coroTest() {
// Globals 是 Coroutines 的一个 builder
GlobalScope.launch {
delay(1000L)//Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
Thread.sleep(2000)
Log.i(CO_TAG, "launch ")
}
Log.i(CO_TAG, "----")
}
控制台输出效果如下:
01-05 11:11:40.373 28131-28131/com.yy.yylite.kotlinshare I/coroutine: ----
01-05 11:11:43.375 28131-3159/com.yy.yylite.kotlinshare I/coroutine: launch
也就是说在子线程 delay 1000 毫秒,然后 sleep 2000 毫秒,之后打印出来了,确实达到了我们理想的状态。
接着看下 Android studio 的 cpu profiler,我们可以看到,这里启动了几个新的子线程:
这里会创建名为DefaultDispatch 的子线程,做个一个简单的实验,不断的重复执行上面的代码,并不会无限创建子线程,看了其内部的线程数也是有约束的。这个可能比直接调度线程池,更加节省资源,也避免了极端情况。(实际上 delay() 是一个非阻塞的挂起函数)
blocking 和 non-blocking 函数
delay{} 是 非阻塞函数,Thread.sleep() 则是阻塞函数,coroutines 中使用 runBlocking{} 作为阻塞函数,例如以下代码:
fun testBlockAndNoBlock() {
//非阻塞,子线程
GlobalScope.launch {
delay(1000)
doLog("no-block")
}
doLog("non block test")
//会阻塞主线程
runBlocking {
delay(3000)
doLog("block")
}
doLog("block test")
}
则控制台输出结果为:
01-05 15:27:37.982 20264-20264/com.yy.yylite.kotlinshare I/coroutine: non block test
01-05 15:27:38.984 20264-20312/com.yy.yylite.kotlinshare I/coroutine: no-block
01-05 15:27:40.983 20264-20264/com.yy.yylite.kotlinshare I/coroutine: block
block test
结果就是使用 runBlocking 会阻塞主线程,那么这个在实际开发中有任何用途吗?实际上,runBlocking{} 不是直接用在协程中的,常常用于桥接一些挂起函数操作,用于顶底函数或者Junit Test中,例如如下代码:
fun testBlock() = runBlocking {
val job= launch {
delay(1000)
doLog("in run block")
}
job.join()
}
这里将 join() 和 launch{} 进行桥接,使他们能够在一个地方执行。
等待
上面提到了可以通过 delay() 来等待一个函数执行,并且是非阻塞的,coroutines 中也提供了另一种等待机制,简单的例子如下:
fun testWaitJob() {
val job = GlobalScope.launch {
delay(2000)
doLog("waite")
}
doLog("main doing")
GlobalScope.launch {
job.join()
doLog("really excute")
}
}
最终输出结果如下,也就是 join()方法
01-05 21:45:53.472 13230-13230/com.yy.yylite.kotlinshare I/coroutine: main doing
01-05 21:45:55.475 13230-13803/com.yy.yylite.kotlinshare I/coroutine: waite
01-05 21:45:55.476 13230-13803/com.yy.yylite.kotlinshare I/coroutine: really excute
任务取消
某个场景下,你开启了一个协程,但是因为一些原因,你要取消这个协程,那么你可以这样处理,使用一下 Job.cancel() 方法取消协程,如下例子:
fun testCancel2() {
doLog("test cancel")
val job = GlobalScope.launch {
for (index in 1..30) {
doLog("print $index")
delay(100)
}
}
doLog("no waite repeat")
GlobalScope.launch {
delay(1000)
doLog("cancel ")
job.cancel()
}
}
控制台输出如下,也就是表示通过 job.cancel() 将执行的协程取消了。
01-05 21:52:20.684 17521-17521/com.yy.yylite.kotlinshare I/coroutine: test cancel
01-05 21:52:20.696 17521-17521/com.yy.yylite.kotlinshare I/coroutine: no waite repeat
01-05 21:52:20.698 17521-17784/com.yy.yylite.kotlinshare I/coroutine: print 1
01-05 21:52:20.801 17521-17788/com.yy.yylite.kotlinshare I/coroutine: print 2
01-05 21:52:20.901 17521-17785/com.yy.yylite.kotlinshare I/coroutine: print 3
01-05 21:52:21.002 17521-17787/com.yy.yylite.kotlinshare I/coroutine: print 4
01-05 21:52:21.105 17521-17787/com.yy.yylite.kotlinshare I/coroutine: print 5
01-05 21:52:21.219 17521-17793/com.yy.yylite.kotlinshare I/coroutine: print 6
01-05 21:52:21.320 17521-17785/com.yy.yylite.kotlinshare I/coroutine: print 7
01-05 21:52:21.421 17521-17788/com.yy.yylite.kotlinshare I/coroutine: print 8
01-05 21:52:21.522 17521-17786/com.yy.yylite.kotlinshare I/coroutine: print 9
01-05 21:52:21.623 17521-17793/com.yy.yylite.kotlinshare I/coroutine: print 10
01-05 21:52:21.706 17521-17799/com.yy.yylite.kotlinshare I/coroutine: cancel
但是实际上, Job 的状态分为以下几种情况:
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | False | False | False |
Active(default initial state) | True | False | False |
Completing(transient state) | True | False | False |
Cancelling(transient state) | False | False | True |
Cancelled(final state) | False | True | True |
Completed(final state) | False | True | False |
那么一个 job 的执行流程如下:
* +-----+ start +--------+ complete +-------------+ finish +-----------+
* | New | -----> | Active | ---------> | Completing | -------> | Completed |
* +-----+ +--------+ +-------------+ +-----------+
* | cancel / fail |
* | +----------------+
* | |
* V V
* +------------+ finish +-----------+
* | Cancelling | --------------------------------> | Cancelled |
* +------------+ +-----------+
那么一个job 的状态根据执行过程,不断发生变化。其次,子job 和父job 相互关联,取消父job 会先依次取消子 job,同样子 Job 取消或者失败也会影响到父 Job 。
launch{} , runBlocking{} ,async{}
launch{} 会在当前线程开启一个新的协程,并且不会阻塞当前线程,同时会返回一个 Job 做为 coroutine 的引用,你可以通过这个 Job 取消对应的 Coroutine。
runBlocking {} 会在开启一个新的协程,并且阻塞当前进程,直到操作完成。这个函数不应该在协程里面使用,它是用来桥接需要阻塞的挂起函数,主要用于 main function 和 junit 测试。也就是说,runBolcking {} 必须用在最上层。
async{} 会在对应的 CoroutineContext 下创建一个新的协程,并且放回一个Deferred,通过 Deferred 可以异步获取结果,也就是调用Deffered 的 await() 方法。
先来看下三者的源码:
//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
}
//runBlocking
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
val currentThread = Thread.currentThread()
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
if (contextInterceptor == null) {
// create or use private event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
// See if context's interceptor is an event loop that we shall use (to support TestContext)
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
//async
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
在 launch 里面会创建一个新的 CoroutineContext,如果没有传入 Context 则使用的 EmptyCoroutineContext,通过 newCoroutineContext() 函数会分配一个默认的 Dispatcher,也就是 Dispatcher.default,默认的全局 Dispatcher,会在jvm 层级共享线程池,会创建等于cpu 内核数目的线程(但是至少创建两个子线程)。接着判断 CoroutineStart 是否 Lazy 模式,如果 Lazy 模式,则该 Coroutine 不会立马执行,需要你主动掉了 Job.start() 之后才会执行。
如果想要了解 Coroutine 原理,请查看下一篇文章 Coroutine 关键类分析