withContext
除了可以在 GlobalScope.launch {}、GlobalScope.async {}
创建协程时设置协程调度器,
与
async {...}.await()
相比withContext
的内存开销更低,因此对于使用async
之后立即调用await
的情况,应当优先使用withContext
。
withTimeout
Kotlin
协程提供了 withTimeout
函数设置超时取消。如果运行超时,取消后会抛出 TimeoutCancellationException
异常。抛出异常的情况下回影响到其他协程,这时候可以使用 withTimeoutOrNull
函数,它会在超时的情况下返回 null
而不抛出异常。
runBlocking {
val result = withContext(coroutineContext) {
withTimeoutOrNull(500) {
delay(1000)
“hello”
}
}
println(result)
}
// 输出结果
hello
yield
如果想要解决上面示例中的问题可以使用 yield
函数。它的作用在于检查所在协程的状态,如果已经取消,则抛出取消异常予以响应。此外它还会尝试出让线程的执行权,给其他协程提供执行机会。
在上面示例中添加 yield
函数:
if (System.currentTimeMillis() >= nextPrintTime) {
yield()
println(“job: I’m sleeping ${i++} …”)
nextPrintTime += 500L
}
// 输出结果
job: I’m sleeping 0 …
job: I’m sleeping 1 …
job: I’m sleeping 2 …
main: I’m tired of waiting!
main: Now I can quit.
协程的作用域
协程作用域:协程作用域主要用于明确协程之间的父子关系,以及对于取消或者异常处理等方面的传播行为。
协程作用域包括以下三种:
-
顶级作用域
:没有父协程的协程所在的作用域为顶级作用域。 -
协同作用域
:协程中启动新的协程,新协程为所在协程的子协程,这种情况下子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常将传递给父协程处理,父协程同时也会被取消。 -
主从作用域
:与协程作用域在协程的父子关系上一致,区别在于处于该作用域下的协程出现未捕获的异常时不会将异常向上传递给父协程。
父子协程间的关系:
-
父协程被取消,则所有子协程均被取消。
-
父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完毕。
-
子协程会继承父协程的协程上下文元素,如果自身有相同
key
的成员,则覆盖对应的key
,覆盖的效果仅限自身范围内有效。
声明顶级作用域:GlobalScope.launch {}
、runBlocking {}
声明协同作用域:coroutineScope {}
声明主从作用域:supervisorScope {}
coroutineScope {}
和 supervisorScope {}
是挂起函数所以它们只能在协程作用域中或挂起函数中调用。
coroutineScope {}
和 supervisorScope {}
的区别在于 SupervisorCoroutine
重写了 childCancelled()
函数使异常不会向父协程传递。
协程并发
通过上文的介绍可以了解到协程其实就是执行在线程上的代码片段,所以线程的并发处理都可以用在协程上,比如 synchorinzed
、CAS
等。而协程本身也提供了两种方式处理并发:
-
Mutex
:互斥锁; -
Semaphore
:信号量。
Mutex
Mutex
类似于 synchorinzed
,协程竞争时将协程包装为 LockWaiter
使用双向链表存储。Mutex
还提供了 withLock
扩展函数,以简化使用:
runBlocking {
v
al mutex = Mutex()
var counter = 0
repeat(10000) {
GlobalScope.launch {
mutex.withLock {
counter ++
}
}
}
Thread.sleep(500) //暂停一会儿等待所有协程执行结束
println(“The final count is $counter”)
}
Semaphore
Semaphore
用以限制访问特定资源的协程数量。
runBlocking {
val semaphore = Semaphore(1)
var counter = 0
repeat(10000) {
GlobalScope.launch {
semaphore.withPermit {
counter ++
}
}
}
Thread.sleep(500) //暂停一会儿等待所有协程执行结束
println(“The final count is $counter”)
}
注意:只有在
permits = 1
时才和Mutex
功能相同。
源码分析
suspend
我们来看 suspend
修饰函数和修饰 lambda
的区别。
挂起函数:
suspend fun suspendFun() {
}
编译成 java
代码如下:
@Nullable
public final Object suspendFun(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
可以看到挂起函数其实隐藏着一个 Continuation
协程实例参数,而这个参数其实就来源于协程体或者其他挂起函数,因此挂起函数只能在协程体内或其他函数内调用了。
suspend
修饰 lambda
表达式:
suspend {}
// 反编译结果如下
Function1 var2 = (Function1)(new Function1((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
switch(this.label) {
case 0:
return Unit.INSTANCE;
default:
}
}
@NotNull
public final Continuation create(@NotNull Continuation completion) {
Function1 var2 = new (completion);
return var2;
}
public final Object invoke(Object var1) {
return (()this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
});
suspend lambda
实际会被编译成 SuspendLambda
的子类。suspendLambda
的继承关系如下图:
通过反编译的代码可以发现我们在协程体内编写的代码最终是在 invokeSuspend
函数内执行的。而在 BaseContinuationImpl
内实现了 Continuation
协程接口的 resumeWidth
函数,并在其内调用了 invokeSuspend
函数。
suspend
关键字的介绍先到这里,接下来我们看协程是如何创建并运行的。
协程是如何被创建的
文件地址
kotlin.coroutines.Continuation.kt
。
Continuation.kt
文件基本属于协程的基础核心了,搞懂了