【kotlin】- delay函數實現原理

簡介

這片文章主要講解kotlindelay函數的實現原理,delay是一個掛起函數。kotlin攜程使用過程中,經常使用到掛起函數,在我學習kotlin攜程的時候,一些現象讓我很是困惑,所以打算從源碼角度來逐一分析。

說明

在分析delay源碼實現過程中,由于對kotlin有些語法還不是很熟悉,所以并不會把每一步將得很透徹,只會梳理一個大致的流程,如果講解有誤的地方,歡迎指出。

例子先行

fun main() = runBlocking {
    println("${treadName()}======start")
    launch {
        println("${treadName()}======delay 1s  start")
        delay(1000)
        println("${treadName()}======delay 1s end")
    }

    println("${treadName()}======delay 3s start")
    delay(3000)
    println("${treadName()}======delay 3s end")
    // 延遲,保活進程
    Thread.sleep(500000)
}

輸出如下:

main======start
main======delay 3s start
main======delay 1s  start
main======delay 1s end
main======delay 3s end

根據日志可以看出:

  1. 日志輸出環境是在主線程。
  2. 執行3s延遲函數后,切換到了launch攜程體執行。
  3. delay掛起函數恢復后執行各自的打印函數。

\color{blue}{疑問}
如果真像打印日志輸出一樣,所以的操作都是在一個線程(主線程)完成,那么問題來了。第一:按照Java線程知識,單線程執行是按照順序的,是單條線的。那么不管delay里是何等騷操作,只要沒有重新起線程,應該不能夠像上面輸入的那樣吧,你說sleep,wait,如果你這么想,那么你可以去補一補Java多線程基礎知識了。猜想1. 難得真有什么我不知道的騷操作可以在一個線程里面同時執行delay和其它代碼,真像很多人說的,攜程性能很好,使用掛起函數可以不用啟動新的線程,就可以異步執行,那真的就很不錯。2. delay啟動了新的線程,上面的現象只不過是進行了線程切換,那么如果多次調用 delay那么豈不是要創建很多線程,這性能問題和資源問題怎么解決。3. delay基于某種任務調度策略。

delay源碼

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T =
    suspendCoroutineUninterceptedOrReturn { uCont ->
        val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
        cancellable.initCancellability()
        block(cancellable)
        cancellable.getResult()
}

cancellable是一個CancellableContinuationImpl對象,執行 block(cancellable),回到下面函數。

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

看一下cont.context.delayget方法

internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

如果get(ContinuationInterceptor)Delay類型對象,那么直接返回該對象,如果不是返回DefaultDelay變量,看一下DefaultDelay初始化可以知道,它是一個DefaultExecutor對象,繼承了EventLoopImplBase類。

runBlocking執行過程中有這樣一行代碼createCoroutineUnintercepted(receiver, completion).intercepted()會被ContinuationInterceptor進行包裝。所以上面cont.context.delay返回的就是被包裝的攜程體上下文。

查看scheduleResumeAfterDelay方法。

    public override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
        val timeNanos = delayToNanos(timeMillis)
        if (timeNanos < MAX_DELAY_NS) {
            val now = nanoTime()
            DelayedResumeTask(now + timeNanos, continuation).also { task ->
                continuation.disposeOnCancellation(task)
                schedule(now, task)
            }
        }
    }

創建DelayedResumeTask對象,在also執行相關計劃任務,看一下schedule方法。

    public fun schedule(now: Long, delayedTask: DelayedTask) {
        when (scheduleImpl(now, delayedTask)) {
            SCHEDULE_OK -> if (shouldUnpark(delayedTask)) unpark()
            SCHEDULE_COMPLETED -> reschedule(now, delayedTask)
            SCHEDULE_DISPOSED -> {} // do nothing -- task was already disposed
            else -> error("unexpected result")
        }
    }

這里返回SCHEDULE_OK,執行unpark函數,這里用到了Java提供的LockSupport線程操作相關知識。

讀取線程

  val thread = thread
  • 如果delay是當前攜程的上下文
    那么把延時任務加入到隊列后,那么又是怎么達到線程延遲呢?;氐?code>runBlocking執行流程,會執行coroutine.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
      }
    

    執行:

     val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
    

    看一下processNextEvent

      override fun processNextEvent(): Long {
          // unconfined events take priority
          if (processUnconfinedEvent()) return 0
          // queue all delayed tasks that are due to be executed
          val delayed = _delayed.value
          if (delayed != null && !delayed.isEmpty) {
              val now = nanoTime()
              while (true) {         
                  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
          val task = dequeue()
          if (task != null) {
              task.run()
              return 0
          }
          return nextTime
      }
    

    從延遲隊列取任務

    val delayed = _delayed.value
    

    掛起當前線程

    parkNanos(this, parkNanos)
    

    這里是一個while循環,當掛起時間到,線程喚醒,繼續從任務隊列中取任務執行。如果還是延遲任務,這根據當前時間點,計算線程需要掛起的時間,這也是為什么多個延遲任務好像是同時執行的。

  • 如果delay是DefaultExecutor
    比如這個例子:攜程上下文沒有像CoroutineStart.DEFAULT那樣進行包裝。

    fun main() {
      GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
           println("${treadName()}======我開始執行了~")
           delay(1000)
            println("${treadName()}======全局攜程~")
        }
        println("${treadName()}======我要睡覺~")
        Thread.sleep(3000)
    }
    

    然后調用DefaultExecutor類中thread的get方法:

      override val thread: Thread
          get() = _thread ?: createThreadSync()
    

    看一下createThreadSync函數

      private fun createThreadSync(): Thread {
          return _thread ?: Thread(this, THREAD_NAME).apply {
              _thread = this
              isDaemon = true
              start()
          }
      }
    

    創建一個叫"kotlinx.coroutines.DefaultExecutor的新線程,并且開始運行。這時候會執行DefaultExecutor中的run方法。在run方法中有這樣一行代碼:

    parkNanos(this, parkNanos)
    

    點進去看看:

    internal inline fun parkNanos(blocker: Any, nanos: Long) {
      timeSource?.parkNanos(blocker, nanos) ?: LockSupport.parkNanos(blocker,   nanos)
    }
    

    調用Java提供的LockSupport.parkNanos(blocker, nanos)方法,阻塞當前線程,實現掛起,當達到阻塞的時間,恢復線程執行。

查看進行中線程情況方法

fun main() {
    println("${treadName()}======doSuspendTwo")
    Thread.sleep(500000)
}

運行main,通過命令jps找到對應Java進程(沒有特別指定,進程名為文件名)號。

...
3406 KotlinCoreutinesSuspendKt
...

執行jstack 進程號查看進程對應的線程資源。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 這篇文章大部分內容來自:https://github.com/Kotlin/kotlinx.coroutines/...
    Jason__Ding閱讀 19,960評論 9 55
  • 在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種...
    業志陳閱讀 1,105評論 0 5
  • 輕量級線程:協程 在常用的并發模型中,多進程、多線程、分布式是最普遍的,不過近些年來逐漸有一些語言以first-c...
    Tenderness4閱讀 6,390評論 2 10
  • package com.example.kotlin_demo import androidx.appcompat...
    多一點童真閱讀 885評論 0 0
  • 本文為協程的開篇作,作者目前對協程的理解仍存在一些疑問,歡迎批評指正。 概念 ?些 API 啟動?時間運?的操作(...
    wanderingGuy閱讀 1,597評論 1 3