此文协程特指在Android平台上的kotlin协程实现,基于1.5.2版本kotlin。
简单过一遍协程的基础类图:
最常使用的协程构建器无外乎四种:
fun test() = runBlocking {
// doing something
}
小tips:单元测试完成后,自定义的打印在测试结果的最底部。
fun testWithContext() = runBlocking {
var str = "ori"
str = withContext(Dispatchers.Default) {
delay(1000)
"1000ori"
}
println(str)
}
// 1秒后打印“1000ori”
在实现方面,withContext会将给定的CoroutineContext与当前协程的CoroutineContext结合,因此可以做到诸如替换CoroutineDispatcher等实现线程切换的操作。
/**
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
*/
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
// compute new context
val oldContext = uCont.context
val newContext = oldContext + context
...
}
fun testLaunch1() = runBlocking {
var str = "ori"
launch {
delay(1000)
str = "1000ori"
}
println(str)
}
// 输出:
ori
可以发现,launch只是开启了新的子协程,但是父协程直接继续执行,如果需要等待子协程完成,则需要使用join:
fun testLaunch2() = runBlocking {
var str = "ori"
val c = launch {
delay(1000)
str = "1000ori"
}
logTime(str)
c.join()
logTime(str)
}
var lastTime = 0L
/**
* 附加时间间隔的println
*/
fun logTime(str: String) {
if (lastTime == 0L) {
println("0-$str")
} else {
println("${System.currentTimeMillis() - lastTime}-$str")
}
lastTime = System.currentTimeMillis()
}
// 输出:
0-ori
1015-1000ori
join会立即挂起协程,等待子协程执行完成。注意,如果仅仅是需要等待执行操作完毕的作业,直接使用withContext,而不是使用launch{…}.join()。
fun testAsync() = runBlocking {
var str = "ori"
val c = async {
delay(1000)
"1000ori"
}
logTime(str)
str = c.await()
logTime(str)
}
// 输出:
0-ori
1013-1000ori
下面重点来看下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
}
其中
1.参数一CoroutineContext可以设置 CoroutineDispatcher 协程运行的线程调度器(默认是Dispatchers.Default)等协程上下文
2.参数二CoroutineStart指定了协程启动模式,其中
var str = "ori"
var startTime = 0L
fun testCoroutineStart() = runBlocking {
logTime()
var str = "ori"
val c = launch(
context = Dispatchers.IO + CoroutineName("TEST"),
start = CoroutineStart.UNDISPATCHED
) {
logTime("launch1")
delay(1000)
str = "1000ori"
logTime("launch2")
}
logTime("main1")
c.join()
logTime("main2")
}
/**
* 附加时间间隔的println
*/
@ExperimentalStdlibApi
private suspend fun logTime(tag: String? = null) {
if (startTime == 0L) {
System.out.format(
"%5s |%10s |%15s |%30s |%10s |%10s %n",
"Time",
"Tag",
"CoroutineName",
"Dispatcher",
"Thread",
"Msg"
)
startTime = System.currentTimeMillis()
} else {
System.out.format(
"%5s |%10s |%15s |%30s |%10s |%10s %n",
"${System.currentTimeMillis() - startTime}",
tag,
"${coroutineContext[CoroutineName]?.name}",
"${coroutineContext[CoroutineDispatcher]}",
"${Thread.currentThread().id}",
msg
)
}
}
// 输出如下
Time | Tag | CoroutineName | Dispatcher | Thread | Msg
2 | launch1 | TEST | Dispatchers.IO | 11 | ori
7 | main1 | null | BlockingEventLoop@3cc8b7e6 | 11 | ori
1027 | launch2 | TEST | Dispatchers.IO | 15 | 1000ori
1032 | main2 | null | BlockingEventLoop@3cc8b7e6 | 11 | 1000ori
可以发现,在delay之前子协程是直接在原线程执行的,delay时交由Dispatchers.IO调度线程,这一点与Dispatchers.Unconfined类似,但是挂起点resume后,UNDISPATCHED会使协程交由 CoroutineContext的CoroutineDispatche调度。
附带个CoroutineDispatche的例子:
launch返回Job对象:
Job控制协程的生命周期,其有三个状态变量:
由于协程是结构化的,因此在completing和cancelling时,会等待所有子协程完成。
协程取消一般使用cancel()或cancelAndJoin()函数:
fun testCancel() = runBlocking {
val c = launch(Dispatchers.Default) {
var i = 0
while (i < 5) {
println("num ${i++}")
delay(500)
}
}
delay(1200)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
num 0
num 1
num 2
try cancel
end
一段协程代码必须协作才能被取消,所有kotlinx.coroutines包中的挂起函数都是可被取消的。协程取消时,会检查子协程的取消,并在取消时抛出CancellationException,CancellationException被默认处理,不会引发协程抛出异常。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的:
fun testCancelCpu() = runBlocking {
val c = launch(Dispatchers.Default) {
var i = 0
var nextPrintTime = System.currentTimeMillis()
while (i < 5) { // 占用CPU
if (System.currentTimeMillis() > nextPrintTime) {
println("num ${i++}")
nextPrintTime += 500
}
}
}
delay(1200)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
num 0
num 1
num 2
try cancel
num 3
num 4
end
可以看出,在cancelAndJoin()之后,由于while还在不断占用CPU,所以还是会继续执行完毕(类似线程的cancel),针对这种情况,可以使用
fun testCancelCpu1() = runBlocking {
val c = launch(Dispatchers.Default) {
var i = 0
var nextPrintTime = System.currentTimeMillis()
while (i < 5) { // 占用CPU
if (!isActive) {
return@launch
} // ensureActive() 可以使用此句替代判断isActive,若已经调用了cancel,此处会抛出CancellationException
if (System.currentTimeMillis() > nextPrintTime) {
println("num ${i++}")
nextPrintTime += 500
}
}
}
delay(1200)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
num 0
num 1
num 2
try cancel
end
/**
Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutines on the same dispatcher to run if possible.
This suspending function is cancellable. If the Job of the current coroutine is cancelled or completed when this suspending function is invoked or while this function is waiting for dispatch, it resumes with a CancellationException. There is a prompt cancellation guarantee. If the job was cancelled while this function was suspended, it will not resume successfully. See suspendCancellableCoroutine documentation for low-level details.
Note: This function always checks for cancellation even when it does not suspend.
Implementation details
If the coroutine dispatcher is Unconfined, this functions suspends only when there are other unconfined coroutines working and forming an event-loop. For other dispatchers, this function calls CoroutineDispatcher.dispatch and always suspends to be resumed later regardless of the result of CoroutineDispatcher.isDispatchNeeded. If there is no CoroutineDispatcher in the context, it does not suspend.
*/
public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
val context = uCont.context
context.ensureActive()
val cont = uCont.intercepted() as? DispatchedContinuation<Unit> ?: return@sc Unit
if (cont.dispatcher.isDispatchNeeded(context)) {
// this is a regular dispatcher -- do simple dispatchYield
cont.dispatchYield(context, Unit)
} else {
// This is either an "immediate" dispatcher or the Unconfined dispatcher
// This code detects the Unconfined dispatcher even if it was wrapped into another dispatcher
val yieldContext = YieldContext()
cont.dispatchYield(context + yieldContext, Unit)
// Special case for the unconfined dispatcher that can yield only in existing unconfined loop
if (yieldContext.dispatcherWasUnconfined) {
// Means that the Unconfined dispatcher got the call, but did not do anything.
// See also code of "Unconfined.dispatch" function.
return@sc if (cont.yieldUndispatched()) COROUTINE_SUSPENDED else Unit
}
// Otherwise, it was some other dispatcher that successfully dispatched the coroutine
}
COROUTINE_SUSPENDED
}
说人话就是挂起当前任务(Job),释放此线程,让其他正在等待的任务公平的竞争获得执行权。
由于yield是个suspend函数,所以肯定也可以感知到cancel()被执行,进而实现协程取消:
fun testCancelCpu1() = runBlocking {
val c = launch(Dispatchers.Default) {
var i = 0
var nextPrintTime = System.currentTimeMillis()
while (i < 5) { // 占用CPU
if (System.currentTimeMillis() > nextPrintTime) {
println("num ${i++}")
nextPrintTime += 500
}
yield() // 事实上此处可以替换成任意一个挂起函数以感知cancel
}
}
delay(1200)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
num 0
num 1
num 2
try cancel
end
在协程取消,需要释放文件、数据库等资源时,可以在finaly中释放:
fun testCancelRelease() = runBlocking {
val c = launch(Dispatchers.Default) {
try {
println("reading from stream")
delay(3000)
println("reading end")
} finally {
println("finally release stream")
}
}
delay(1000)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
reading from stream
try cancel
finally release stream
end
对于实现了Closeable接口的类,如各种Stream、Buffer等,可以直接使用.use{}实现自动在finally中调用close()方法。
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
...
}
fun testCancelRelease() = runBlocking {
val c = launch(Dispatchers.Default) {
FileInputStream(File("build.gradle")).use {
println("reading from stream")
delay(3000)
println("reading end")
}
}
delay(1000)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
reading from stream
try cancel
end
特别注意,在finally中,调用挂起函数会直接抛出 CancellationException,因为挂起函数都是可取消的:
fun testCancelRelease() = runBlocking {
val c = launch(Dispatchers.Default) {
try {
println("reading from stream")
delay(3000)
println("reading end")
} finally {
println("finally release stream")
delay(2000)
println("release end")
}
}
delay(1000)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
reading from stream
try cancel
finally release stream
end
如果确实需要在finally中执行挂起,可以使用withContext(NonCancellable) {}执行:
fun testCancelRelease() = runBlocking {
val c = launch(Dispatchers.Default) {
try {
println("reading from stream")
delay(3000)
println("reading end")
} finally {
withContext(NonCancellable) {
println("finally release stream")
delay(2000)
println("release end")
}
}
}
delay(1000)
println("try cancel")
c.cancelAndJoin()
println("end")
}
// 输出
reading from stream
try cancel
finally release stream
release end
end
此外,还可以使用withTimeout执行指定超时时间的等待作业,如果不希望超时后会抛出超时异常,可以使用withTimeoutOrNull在超时时返回null。
揭秘kotlin协程中的CoroutineContext
kotlin学习