Kotlin協程-特殊的阻塞協程

阻塞協程是種特殊的協程啓動方式,一般是用 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的問題出現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章