阻塞協程是種特殊的協程啓動方式,一般是用 runBlocking{} 擴起來一段協程。
fun main() = runBlocking {
launch {
println("launch start")
delay(100L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World Thread: ${Thread.currentThread().name}")
println("World!") // 在延遲後打印輸出
}
println("Hello!")
println("Hello Thread: ${Thread.currentThread().name}")
Thread.sleep(400L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
println("out launch done")
}
這段代碼的執行結果是
Hello!
Hello Thread: main
out launch done
launch start
World Thread: main
World!
代碼包含了runBlocking{}和launch{}兩段coroutine,父子關係。首先是父協程得到執行,然後纔是子協程。
重點是這兩段協程都在同一個線程main裏完成。這裏就帶來一個有趣的問題,
runBLocking{}和平時常用的launch有什麼區別?
你可以嘗試把上面的launch{},改成 GlobalScope.launch{},看看結果有什麼不一樣。這裏先給出答案,改用GlobalScope.launch之後,子協程會在一個獨立的線程裏運行。
runBlocking
在kotlin協程官網上對於這個api的解釋是橋接阻塞與非阻塞的世界。這個機翻中文我迷惑了很久,一直不能明白它的意思。於是就去翻了源碼的註釋,
/**
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
* This function should not be used from a coroutine. It is designed to bridge regular blocking code
* to libraries that are written in suspending style, to be used in `main` functions and in tests.
...
*/
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
blabla一堆,意思是也跟"橋接阻塞與非阻塞的世界"差不多,只是多了一句“會阻塞當前線程直到coroutine完成”。但在我驗證之後發現實際情況跟註釋有點不同,如果在 runBlocking 中開一個 GlobalScope.launch,並且在裏面延時很久,那麼外面的線程其實是不會等待 GlobalScope 裏的協程完成的。弄明白這點需要理解這個特殊的阻塞協程 runBlocking 的原理。
創建
runBlocking的創建在jvm包下的Builders.kt中,
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()
}
首先它會判斷當前是否上下文有現成的Dispatcher,或者叫Intercepter,如果有的話就直接拿過來。沒有的話就使用默認的eventloop。EventLoop是協程裏對阻塞型coroutine進行調度的默認調度器。runBlocking和launch的主要區別就靠EventLoop實現。
在創建完coroutine後就進入派發流程了,這部分和Kotlin協程-一個協程的生命週期中的邏輯比較相似,下面也會講到。
最後會調用 joinBlocking() 去執行coroutine,我們放到第三部分執行
分析。
派發
EventLoop是一個特殊的調度類型。它的公用實現在 EventLoop.common.kt 中,
@ThreadLocal
internal object ThreadLocalEventLoop {
private val ref = CommonThreadLocal<EventLoop?>()
internal val eventLoop: EventLoop //eventloop對象
get() = ref.get() ?: createEventLoop().also { ref.set(it) }
internal fun currentOrNull(): EventLoop? =
ref.get()
internal fun resetEventLoop() {
ref.set(null)
}
internal fun setEventLoop(eventLoop: EventLoop) {
ref.set(eventLoop)
}
}
createEventLoop()是個expect函數,用來獲取平臺上的實際實現。函數聲明也在這個文件中,
internal expect fun createEventLoop(): EventLoop
而eventloop對象,是保存在ThreadLocal中的,意味着這個對象在每個線程裏都會有一個,而且互不影響。每個線程都可以起一個獨立的阻塞協程隊列。
在jvm平臺上的eventloop對象是在jvm包下的EventLoop.kt中,它的默認實現是 BlockingEventLoop
internal class BlockingEventLoop(
override val thread: Thread
) : EventLoopImplBase()
internal actual fun createEventLoop(): EventLoop = BlockingEventLoop(Thread.currentThread())
按慣例最後會去執行派發器的dispatch()方法,因爲有了之前的分析經驗,這裏直接到BlockingEventLoop父類EventLoopImplBase的dispatch()函數,
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block) //重載 dispatch函數,調用入隊函數
public fun enqueue(task: Runnable) {
if (enqueueImpl(task)) { //入隊
// todo: we should unpark only when this delayed task became first in the queue
unpark()
} else {
DefaultExecutor.enqueue(task)
}
}
@Suppress("UNCHECKED_CAST")
private fun enqueueImpl(task: Runnable): Boolean { //真正入隊
_queue.loop { queue ->
if (isCompleted) return false // fail fast if already completed, may still add, but queues will close
when (queue) {
null -> if (_queue.compareAndSet(null, task)) return true //在這裏入隊
is Queue<*> -> {
when ((queue as Queue<Runnable>).addLast(task)) {
Queue.ADD_SUCCESS -> return true
Queue.ADD_CLOSED -> return false
Queue.ADD_FROZEN -> _queue.compareAndSet(queue, queue.next())
}
}
else -> when {
queue === CLOSED_EMPTY -> return false
else -> {
// update to full-blown queue to add one more
val newQueue = Queue<Runnable>(Queue.INITIAL_CAPACITY, singleConsumer = true)
newQueue.addLast(queue as Runnable)
newQueue.addLast(task)
if (_queue.compareAndSet(queue, newQueue)) return true
}
}
}
}
}
BlockingEventLoop 的入隊函數 enqueueImpl 邏輯比較簡單,通過when判斷queue的類型走不同的邏輯。實際上這段邏輯還不穩定,仔細分析會發現,queue 在blocking eventloop 的場景下,只會有 null一種可能。所以它的入隊,實際上最後都會走這段代碼。
null -> if (_queue.compareAndSet(null, task)) return true
執行
回到上面的創建階段,最後會執行 joinBlocking
fun joinBlocking(): T {
registerTimeLoopThread()
try {
eventLoop?.incrementUseCount()
try {
while (true) {
@Suppress("DEPRECATION")
if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE //執行隊列裏的下一個任務
// note: process next even may loose unpark flag, so check if completed before parking
if (isCompleted) break
parkNanos(this, parkNanos)
}
} finally { // paranoia
eventLoop?.decrementUseCount()
}
} finally { // paranoia
unregisterTimeLoopThread()
}
// now return result
val state = this.state.unboxState()
(state as? CompletedExceptionally)?.let { throw it.cause }
return state as T
}
processNextEvent()會從上面的queue中取出任務並且執行。因爲eventloop在jvm上的實現是BlockingEventLoop,它的父類是 EventLoopImplBase,所以processNextEvent()在 EventLoop.common.kt 中,
override fun processNextEvent(): Long {
// unconfined events take priority
if (processUnconfinedEvent()) return nextTime
// queue all delayed tasks that are due to be executed
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) { //判斷是否到延時時間,否則重新入隊
val now = nanoTime()
while (true) {
// make sure that moving from delayed to queue removes from delayed only after it is added to queue
// to make sure that 'isEmpty' and `nextTime` that check both of them
// do not transiently report that both delayed and queue are empty during move
delayed.removeFirstIf {
if (it.timeToExecute(now)) {//重新入隊
enqueueImpl(it)
} else
false
} ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
}
}
// then process one event from queue
dequeue()?.run() //出隊並執行
return nextTime
}
dequeue()的實現也相對簡單,跟入隊的邏輯差不多
@Suppress("UNCHECKED_CAST")
private fun dequeue(): Runnable? {
_queue.loop { queue ->
when (queue) {
null -> return null
is Queue<*> -> {
val result = (queue as Queue<Runnable>).removeFirstOrNull()
if (result !== Queue.REMOVE_FROZEN) return result as Runnable?
_queue.compareAndSet(queue, queue.next())
}
else -> when {
queue === CLOSED_EMPTY -> return null
else -> if (_queue.compareAndSet(queue, null)) return queue as Runnable //出隊並把當前queue設爲null
}
}
}
}
上面說過,在BlockingEventLoop場景下,queue的入隊只會有null一種可能。而這裏也是一樣,只會從else進去。
雖然queue名義上是個隊列,它也支持隊列的邏輯,比如在 is Queue<*>
這個分支上,它的實現是個隊列。但現在可以把它當做個容量爲1的隊列。
之後就是task.run的流程了,和之前的分析沒什麼區別。
BlockingEventLoop的特殊性
上面的分析可以看出一個問題,queue不是個隊列,而且每次它都只會在 null->task 之間轉換。也就是說,不管什麼時候,queue的長度只會是1或者0.
這個問說明,runBLocking{}這種協程,它的運行邏輯是先把父協程放隊列裏,然後取出來執行,執行完畢再把子協程入隊,再出隊子協程,用同樣的方式遞歸。雖然這種方式能保證整體是個阻塞流程,但是設計上不夠優雅。猜測是爲了避免協程嵌套太多,導致stack over flow的問題出現。