協程作用域、上下文與調度

協程作用域CoroutineScope

在 Android 環境中,通常每個界面(Activity、Fragment 等)啟動的 Coroutine 只在該界面有意義,如果用戶在等待 Coroutine 執行的時候退出了這個界面,則再繼續執行這個 Coroutine 可能是沒必要的。另外 Coroutine 也需要在適當的 context 中執行,否則會出現錯誤,比如在非 UI 線程去訪問 View。 所以 Coroutine 在設計的時候,要求在一個范圍(Scope)內執行,這樣當這個 Scope 取消的時候,里面所有的子 Coroutine 也自動取消。所以要使用 Coroutine 必須要先創建一個對應的 CoroutineScope

CoroutineScope 接口

CoroutineScope 是一個接口,要是查看這個接口的源代碼的話就發現這個接口里面只定義了一個屬性 CoroutineContext

public interface CoroutineScope {
    // Scope 的 Context
    public val coroutineContext: CoroutineContext
}

所以 CoroutineScope 只是定義了一個新 Coroutine 的執行 Scope。每個協程coroutine builder(launch 、async等) 都是 CoroutineScope 的擴展方法,并且自動的繼承了當前 Scope 的 coroutineContext 和取消操作。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
   ......
}

每個 coroutine builder 和 scope 方法(withContext、coroutineScope 等)都使用自己的 Scope 和 自己管理的 Job 來運行提供給這些函數的代碼塊。并且也會等待該代碼塊中所有子 Coroutine 執行,當所有子 Coroutine 執行完畢并且返回的時候, 該代碼塊才執行完畢,這種行為被稱之為 “structured concurrency”。

全局作用域GlobalScope

GlobalScope 是 CoroutineScope 的一個單例實現,其代碼也是非常簡單的:

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

用法

        GlobalScope.launch(Dispatchers.Main) {
            delay(7000)
            val result = "content"
            //主線程里更新 UI
            text.text = result
        }

該實例所用的 CoroutineContext 是一個 EmptyCoroutineContext 實例(這也是一個單例 object 對象)。由于 GlobalScope 對象沒有和應用生命周期組件相關聯,需要自己管理 GlobalScope 所創建的 Coroutine,所以一般而言我們不直接使用 GlobalScope 來創建 Coroutine。

與生命周期綁定的作用域

一般而言,在應用中具有生命周期的組件應該實現 CoroutineScope 接口,并負責該組件內 Coroutine 的創建和管理。例如對于 Android 應用來說,可以在 Activity 中實現 CoroutineScope 接口, 例如:

class ScopedActivity : Activity(), CoroutineScope {
    lateinit var job: Job
    // CoroutineScope 的實現
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()

    /*
     * 注意 coroutine builder 的 scope, 如果 activity 被銷毀了或者該函數內創建的 Coroutine
     * 拋出異常了,則所有子 Coroutines 都會被自動取消。不需要手工去取消。
     */
      launch { // <- 自動繼承當前 activity 的 scope context,所以在 UI 線程執行
          val ioData = async(Dispatchers.IO) { // <- launch scope 的擴展函數,指定了 IO dispatcher,所以在 IO 線程運行
            // 在這里執行阻塞的 I/O 耗時操作
          }
        // 和上面的并非 I/O 同時執行的其他操作
          val data = ioData.await() // 等待阻塞 I/O 操作的返回結果
          draw(data) // 在 UI 線程顯示執行的結果
      }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 當 Activity 銷毀的時候取消該 Scope 管理的 job。
        // 這樣在該 Scope 內創建的子 Coroutine 都會被自動的取消。
        job.cancel()
    }


}

由于所有的 Coroutine 都需要一個 CoroutineScope,所以為了方便創建 Coroutine,在 CoroutineScope 上有很多擴展函數,比如 launch、async、actor、cancel 等。

MainScope

在 Android 中會經常需要實現這個 CoroutineScope,所以為了方便開發者使用, 標準庫中定義了一個 MainScope() 函數,該函數定義了一個使用 SupervisorJob 和 Dispatchers.Main 為 Scope context 的實現。所以上面的代碼可以簡化為:

class ScopedActivity : Activity(), 
    CoroutineScope by MainScope(){ // 使用 by 指定代理實現

    override fun onDestroy() {
        super.onDestroy()
        cancel() // 調用 CoroutineScope 的 cancel 函數
    }

在mvvm模式使用作用域

class ViewModelOne : ViewModel() {

    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    val mMessage: MutableLiveData<String> = MutableLiveData()

    fun getMessage(message: String) {
        uiScope.launch {
            val deferred = async(Dispatchers.IO) {
                delay(2000)
                "post $message"
            }
            mMessage.value = deferred.await()
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
}

ViewModelScope 方式

AndroidX Lifecycle v2.1.0 在 ViewModel 中引入 viewModelScope,當 ViewModel 被銷毀時它會自動取消協程任務,這個特性真的好用。viewModelScope 管理協程的方式與我們在 ViewModel 引入協程的方式一樣,代碼實現如下:

class MyViewModel : ViewModel() {
  
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

協程上下文CoroutineContext

CoroutineContext是一個接口,我們常用到的Job, Dispatchers都是實現了該接口的類,此外還包括 CoroutineName 和CoroutineId等類。

@SinceKotlin("1.3")
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        public val key: Key<*>
        ...
    }
}

類似于一個以 Key 為索引的 List:

CoroutineContext 作為一個集合,它的元素就是源碼中看到的 Element,每一個 Element 都有一個 key,因此它可以作為元素出現,同時它也是 CoroutineContext 的子接口,因此也可以作為集合出現。

我們看下實現了上下文接口的Job類

public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> {
        init {
            /*
             * Here we make sure that CoroutineExceptionHandler is always initialized in advance, so
             * that if a coroutine fails due to StackOverflowError we don't fail to report this error
             * trying to initialize CoroutineExceptionHandler
             */
            CoroutineExceptionHandler
        }
    }
}

可以看到Job實現關系是Job<=Element<=CoroutineContext

    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }

協程生成器 Coroutine builders

我們知道生成協程的方式有很多種,比如 launch、async、runBlocking等,他們都是
CoroutineScope的擴展方法,且都會創建一個新的協程
下面我們拿launch開啟一個協程的例子來了解下如何設置上下文

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    ...
}

我們為上下文添加調度器與上下文名字

        launch(context = Dispatchers.Main.plus(CoroutineName("jason"))) {
            val job = coroutineContext.get(Job)
            val dispatcher = coroutineContext.get(ContinuationInterceptor)
            val name = coroutineContext.get(CoroutineName)
            println("job:$job")
            println("dispatcher:$dispatcher")
            println("name:$name")
        }

打印輸出結果

job:StandaloneCoroutine{Active}@ad739cb
dispatcher:Main
name:CoroutineName(jason)

上下文切換器withContext

與 launch、async、runBlocking 等不同,withContext 不會創建新的協程。
withContext 允許切換協程上下文,使用時必須傳遞一個 CoroutineContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
  ...
}

看下例子

        launch(context = Dispatchers.IO.plus(CoroutineName("c1"))) {
            val dispatcher = coroutineContext[ContinuationInterceptor]
            val name = coroutineContext[CoroutineName]
            println("scope:$this")
            println("dispatcher:$dispatcher")
            println("name:$name")
            withContext(Dispatchers.Main.plus(CoroutineName("c2"))){
                val dispatcher2 = coroutineContext[ContinuationInterceptor]
                val name2 = coroutineContext[CoroutineName]
                println("scope2:$this")
                println("dispatcher2:$dispatcher2")
                println("name2:$name2")
            }
        }

日志輸出結果

scope:StandaloneCoroutine{Active}@60d84a6
dispatcher:LimitingDispatcher@903ee7[dispatcher = DefaultDispatcher]
name:CoroutineName(c1)

scope2:DispatchedCoroutine{Active}@e3f5a94
dispatcher2:Main
name2:CoroutineName(c2)

續體攔截器ContinuationInterceptor

攔截器也是上下文的一種,它實現了上下文接口
我們可以用攔截器打日志等。調度器就是基于攔截器實現的,換句話說調度器就是攔截器的一種。

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    ...
}

下面我們自己定義一個攔截器放到我們的協程上下文中,看看會發生什么。

class MyContinuationInterceptor: ContinuationInterceptor {
    override val key = ContinuationInterceptor
    override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}

class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
    override val context = continuation.context
    override fun resumeWith(result: Result<T>) {
        Log.d("jason","result=$result" )
        continuation.resumeWith(result)
    }
}
        launch {
            launch(MyContinuationInterceptor()) {
                log(1)
                val deferred = async {
                    log(2)
                    delay(1000)
                    log(3)
                    "我是返回值"
                }
                log(4)
                val result = deferred.await()
                log("5. $result")
            }.join()
            log(6)
        }

我們通過 launch 啟動了一個協程,為它指定了我們自己的攔截器作為上下文,緊接著在其中用 async 啟動了一個協程,async 與 launch 從功能上是同等類型的函數,它們都被稱作協程的 Builder 函數,不同之處在于 async 啟動的 Job 也就是實際上的 Deferred 可以有返回結果,可以通過 await 方法獲取。
輸出日志

23:02:58.595 5241-5241/com.example.mytest D/jason: result=Success(kotlin.Unit) //1
23:02:58.596 5241-5241/com.example.mytest D/jason: 1
23:02:58.598 5241-5241/com.example.mytest D/jason: result=Success(kotlin.Unit) //2
23:02:58.598 5241-5241/com.example.mytest D/jason: 2 
23:02:58.602 5241-5241/com.example.mytest D/jason: 4
23:02:59.602 5241-5273/com.example.mytest D/jason: result=Success(kotlin.Unit) //3
23:02:59.602 5241-5273/com.example.mytest D/jason: 3
23:02:59.603 5241-5273/com.example.mytest D/jason: result=Success(我是返回值) //4
23:02:59.604 5241-5273/com.example.mytest D/jason: 5. 我是返回值
23:02:59.605 5241-5241/com.example.mytest D/jason: 6

首先,這段代碼中一共有4次調度機會,所有協程啟動的時候,都會有一次 Continuation.resumeWith 的操作,這一次操作對于調度器來說就是一次調度的機會,我們的協程有機會調度到其他線程的關鍵之處就在于此。 ①、② 兩處都是這種情況。

其次,delay 是掛起點,1000ms 之后需要繼續調度執行該協程,因此就有了 ③ 處的日志。

最后,④ 處的日志就很容易理解了,正是我們的返回結果。

如果我們在攔截器當中自己處理了線程切換,那么就實現了自己的一個簡單的調度器,大家有興趣可以自己去嘗試。

協程任務執行環境-Dispatcher(調度器)

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ...
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    ...
}

它本身是協程上下文的子類,同時實現了攔截器的接口, dispatch 方法會在攔截器的方法 interceptContinuation 中調用,進而實現協程的調度。所以如果我們想要實現自己的調度器,繼承這個類就可以了,不過通常我們都用現成的
現成的調度器有:

調度器名稱 使用線程
Main UI線程
IO 線程池
Default 線程池
Unconfined 直接執行

也能使用這種方式簡單的創建一個自定義的協程調度器

val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()

這里我們主要看一下我們最常使用的Dispatcher.Main和Dispatcher.IO兩個派發器。


image.png

Dispatcher.Main

Dispatcher.Main沒有默認實現,依賴于各個平臺的實現,如果沒有引入android依賴包,則會拋異常提示,那么kotlin是怎么支持這種動態的類呢?

  1. 首先kotlin提供了一個工廠類接口,用來創建MainDispatcher
public interface MainDispatcherFactory {
    fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher
}
  1. 然后再看反編譯的源碼
public final <S> List<S> loadProviders$kotlinx_coroutines_core(@NotNull Class<S> paramClass, @NotNull ClassLoader paramClassLoader) {
    //從apk的META-INF/services/文件夾下那類名
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("META-INF/services/");
    stringBuilder.append(paramClass.getName());
    Enumeration enumeration = paramClassLoader.getResources(stringBuilder.toString());
    ArrayList arrayList = Collections.list(enumeration);
    Iterable iterable = (Iterable)arrayList;
    Collection collection = (Collection)new ArrayList();
    for (URL uRL : iterable) {
      FastServiceLoader fastServiceLoader = INSTANCE;
      Intrinsics.checkExpressionValueIsNotNull(uRL, "it");
      CollectionsKt.addAll(collection, (Iterable)fastServiceLoader.parse(uRL));
    } 
    collection = CollectionsKt.toSet((Iterable)collection);
    iterable = (Iterable)collection;
    collection = (Collection)new ArrayList(CollectionsKt.collectionSizeOrDefault(iterable, 10));
    //將類名解析為實例對象
    for (String str : iterable)
      collection.add(INSTANCE.getProviderInstance(str, paramClassLoader, paramClass)); 
    return (List)collection;
  }

MainDispatcher的factory會從apk的META-INF/services/文件夾下獲取。

  1. 再看編譯生成的apk文件的該文件夾內容


    image.png

    所以android的依賴包是通過向該文件注冊類名實現的注冊類,并且factory類為AndroidDispatcherFactory。

  2. 最后我們再來看下AndroidDispatcherFactory類
internal class AndroidDispatcherFactory : MainDispatcherFactory {
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) = HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")
}
internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    public constructor(
        handler: Handler,
        name: String? = null
    ) : this(handler, name, false)

    //android中需要向主looper進行提交調度
    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        return !invokeImmediately || Looper.myLooper() != handler.looper
    }

    //通過持有主線程looper的handler進行調度
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }
    ...
}

很清楚,就是用持有主線程looper的handler進行任務的調度,確保任務會在主線程執行。

Dispatcher.IO

internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    val IO = blocking(systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)))
    public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
        return LimitingDispatcher(this, parallelism, TaskMode.PROBABLY_BLOCKING)
    }
}

Dispatcher.IO是一個LimitingDispatcher實例,他可以控制同時并發任務數,默認為64個,即最多有64個任務同時在運行。

private class LimitingDispatcher(
    val dispatcher: ExperimentalCoroutineDispatcher,
    val parallelism: Int,
    override val taskMode: TaskMode
) : ExecutorCoroutineDispatcher()

而LimitingDispatcher內部真正調度任務的dispatcher是一個ExperimentalCoroutineDispatcher對象。

open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE,
        schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    private var coroutineScheduler = createScheduler()

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            DefaultExecutor.dispatch(context, block)
        }

    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
}

我們看到,該dispatcher里面的真正的線程池,是CoroutineScheduler對象,而核心線程數和最大線程數,取決于可用CPU的數量。

internal val CORE_POOL_SIZE = systemProp(
    "kotlinx.coroutines.scheduler.core.pool.size",
    AVAILABLE_PROCESSORS.coerceAtLeast(2), // !!! at least two here
    minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)
internal val AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors()

協程調度器-CoroutineScheduler

這里我們挑幾個小細節看一下CoroutineScheduler是如何來優化對線程的使用的。

i. 盡量使用當前線程

private fun submitToLocalQueue(task: Task, fair: Boolean): Int {
        val worker = currentWorker() ?: return NOT_ADDED
            ...
        worker.localQueue.add(task, globalQueue)
            ...
}
private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this }

如果當前線程是Dispatcher.IO開啟的工作線程,那么任務優先交由該線程的任務隊列,等待處理。

ii. 雙重隊列

fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, fair: Boolean = false) {
  ...
  when (submitToLocalQueue(task, fair)) {
    ADDED -> return
    NOT_ADDED -> {
      //本地隊列已滿,放入全局隊列,所有線程可取
      globalQueue.addLast(task)
    }
    else -> requestCpuWorker() // ask for help
  }
}

如果工作線程本地隊列無限大,一味的放入本地隊列的話,可能會造成單一線程工作,效率極低,于是每個工作線程有固定大小的queue,滿了之后,會放到全局queue中,等待任意空閑工作線程執行。

iii.搶占其他線程的任務

//工作線程Worker類
override fun run() {
  while (!isTerminated && state != WorkerState.TERMINATED) {
    val task = findTask()
    ...
  }
  ...
}
private fun findTaskWithCpuPermit(): Task? {
    ...
  //從本地queue獲取任務
  localQueue.poll()?.let { return it }
  //從全局queue獲取任務
  if (!globalFirst) globalQueue.removeFirstOrNull()?.let { return it }
  //搶占其他線程任務
  return trySteal()
}
private fun trySteal(): Task? {
  ...
  //隨機一個工作線程
  if (stealIndex == 0) stealIndex = nextInt(created)
  ...
  val worker = workers[stealIndex]
  if (worker !== null && worker !== this) {
    //將其queue里的任務放到自己queue中
    if (localQueue.trySteal(worker.localQueue, globalQueue)) {
      return localQueue.poll()
    }
  }
  return null
}

如果一個工作線程的本地queue和全局queue都沒有任務了,但是其他線程的queue還有任務,此時讓其空閑,一是沒有充分利用線程提升工作效率,二是線程的空閑狀態切換需要開銷,所以此時會嘗試從任一工作線程的queue中取出任務,放入自己的queue中執行。

以上三點的相互配合,可以充分利用線程資源,避免過多線程的使用及開銷,也保證了多任務時的工作效率。

協程執行過程源碼追蹤分析

我們以一個請求數據后在主線程更新界面的代碼來進行分析

 fun setUpUI(){
        GlobalScope.launch(Main) { 
            val dataDeferred  = requestDataAsync()
            doSomethingElse()
            val data = dataDeferred.await()
            processData(data)
        }
        Thread.sleep(1000)
        doSomethingElse2()
    }

    fun requestDataAsync():Deferred<String>{
        // 啟動一個異步協程去執行耗時任務
        return GlobalScope.async { 
            requestData()
        }
    }  

    fun doSomethingElse2(){
        println("doSomethingElse2")
    }

編譯后生成偽代碼

final void setUpUI() {
        BuildersKt__Builders_commonKt.launch$default(GlobalScope.INSTANCE, 
        Dispatchers.getMain(), 
        null,
        // 傳入的是一個 KotlinTest$setUpUI.KotlinTest$setUpUI$1 對象
        (Function2)new KotlinTest$setUpUI.KotlinTest$setUpUI$1(this, (Continuation)null), 2, null);
        this.doSomethingElse2();
}

final class setUpUI$1 extends SuspendLambda implements Function2{
    public final Object invokeSuspend(Object result) {
        switch (this.label) {
            case 0:
                doSomethingElse()
                // 新建并啟動 async 協程
                Deferred async$default = BuildersKt.async$default(coroutineScope, (CoroutineContext) Dispatchers.getDefault(), null, (Function2) new 1(null), 2, null);
                this.label = 1;
                // 如果 async 協程還沒完成為掛起狀態 則直接返回,等待下次喚醒重入
                if (async$default.await(this) == coroutine_suspended) {
                    return coroutine_suspended;
                }
                break;
            case 1:
                val data = result;
                processData(data)
                break;
        }
    }
}

可以看到傳入到 launch 函數第四個參數位置的是一個編譯后生成的 SuspendLambda 類實例setUpUI$1,SuspendLambda 本質上是一個續體 Continuation,而 Continuation 是一個有著恢復操作的接口

/**
 * 在一個掛起點之后可以返回類型T值的續集continuation的接口
 * Interface representing a continuation after a suspension point that returns value of type `T`.
 */
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * Context of the coroutine that corresponds to this continuation.
     */
    // todo: shall we provide default impl with EmptyCoroutineContext?
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

SuspendLambda 繼承結構如下
SuspendLambda > ContinuationImpl > BaseContinuationImpl > Continuation

每一層封裝對應添加了不同的功能,我們先忽略掉這些功能細節,著眼于我們的主線,繼續跟進launch 函數執行過程,由于第二個參數是默認值,所以創建的是 StandaloneCoroutine, 調用鏈如下:

coroutine.start(start, coroutine, block)
-> CoroutineStart.start(block, receiver, this)
-> CoroutineStart.invoke(block: suspend () -> T, completion: Continuation<T>)
-> block.startCoroutineCancellable(completion)
-> createCoroutineUnintercepted(completion).intercepted().resumeCancellable(Unit)
我們看最后創建了一個協程,并鏈式調用 intercepted、resumeCancellable 方法,利用協程上下文中的續體攔截器 ContinuationInterceptor 對協程的執行進行攔截,intercepted 實際上調用的是 ContinuationImpl 的 intercepted 方法

internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
    ...
    public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
    ...
}

context[ContinuationInterceptor]?.interceptContinuation調用的是 CoroutineDispatcher 的 interceptContinuation 方法

public final <T> Continuation<T> interceptContinuation(@NotNull final Continuation<? super T> continuation) {
        Intrinsics.checkParameterIsNotNull(continuation, "continuation");
        return new DispatchedContinuation<T>(this, continuation);
    }

最終創建了一個 DispatchedContinuation 可分發的協程實例,我們繼續看resumeCancellable 方法

internal fun <T> Continuation<T>.resumeCancellable(value: T) = when (this) {
    // 判斷是否是DispatchedContinuation 根據我們前面的代碼追蹤 這里是DispatchedContinuation
    is DispatchedContinuation -> resumeCancellable(value)
    else -> resume(value)
}

inline fun resumeCancellable(value: T) {
        // 判斷是否需要線程調度 
        if (dispatcher.isDispatchNeeded(context)) {
            _state = value
            resumeMode = MODE_CANCELLABLE
            dispatcher.dispatch(context, this)
        } else {
            UndispatchedEventLoop.execute(this, value, MODE_CANCELLABLE) {
                if (!resumeCancelled()) {
                    resumeUndispatched(value)
                }
            }
        }
    }

最終走到 dispatcher.dispatch(context, this) 而這里的 dispatcher 就是通過工廠方法創建的 HandlerDispatcher ,dispatch() 函數第二個參數this是一個runnable這里為 DispatchedTask

HandlerDispatcher

/**
 * Implements [CoroutineDispatcher] on top of an arbitrary Android [Handler].
 */
internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    ...
    //  最終執行這里的 dispatch方法 而handler則是android中的 MainHandler
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }
    ...
}

這里借用 Android 的主線程消息隊列來在主線程中執行 block Runnable 而這個 Runnable 即為 DispatchedTask

internal abstract class DispatchedTask<in T>(
    @JvmField var resumeMode: Int
) : SchedulerTask() {
    ...
    public final override fun run() {
          ...
            withCoroutineContext(context, delegate.countOrElement) {
                if (job != null && !job.isActive)
                    // 異常情況下
                    continuation.resumeWithException(job.getCancellationException())
                else {
                    val exception = getExceptionalResult(state)
                    if (exception != null)
                        // 異常情況下
                        continuation.resumeWithStackTrace(exception)
                    else
                        // 正常情況下走到這一步
                        continuation.resume(getSuccessfulResult(state))
                }
            }
             ...
    }
}

@InlineOnly public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

internal abstract class BaseContinuationImpl(...) {
    // 實現 Continuation 的 resumeWith,并且是 final 的,不可被重寫
    public final override fun resumeWith(result: Result<Any?>) {
        ...
        val outcome = invokeSuspend(param)
        ...
    }
    // 由編譯生成的協程相關類來實現,例如 setUpUI$1
    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}

最終調用到 continuation.resumeWith() 而 resumeWith() 中會調用 invokeSuspend,即之前編譯器生成的 SuspendLambda 中的 invokeSuspend 方法

final class setUpUI$1 extends SuspendLambda implements Function2{
    public final Object invokeSuspend(Object result) {
        switch (this.label) {
            case 0:
                doSomethingElse()
                // 新建并啟動 async 協程
                Deferred async$default = BuildersKt.async$default(coroutineScope, (CoroutineContext) Dispatchers.getDefault(), null, (Function2) new 1(null), 2, null);
                this.label = 1;
                // 如果 async 協程還沒完成為掛起狀態 則直接返回,等待下次喚醒重入
                if (async$default.await(this) == coroutine_suspended) {
                    return coroutine_suspended;
                }
                break;
            case 1:
                val data = result;
                processData(data)
                break;
        }
    }
}

這段代碼是一個狀態機機制,每一個掛起點都是一種狀態,協程恢復只是跳轉到下一個狀態,掛起點將執行過程分割成多個片段,利用狀態機的機制保證各個片段按順序執行。

如果沒有掛起點就只有一個初始狀態,類似于callback回調,所以對應了之前我們分析的非阻塞的異步底層實現其實也是一種callback回調,只不過有多個掛起點時就會有多個callback回調,我們把多個callback回調封裝成了一個狀態機。

協程的掛起

從協程的調度過程我們知道,調度后會到編譯器生成的 SuspendLambda 的 invokeSuspend 方法中的一個掛起方法,以例子中的await為例

if (async$default.await(this) == coroutine_suspended) {
        //目前還在掛起中,則return等待掛起結束后的invokeSuspend
        return coroutine_suspended;
}

async 也是一個協程,如果狀態為掛起coroutine_suspended,則執行流直接 return 返回,如果已達到完成狀態直接跳轉下一個狀態 case 1 最終走完整個協程代碼塊。

這里需要注意的是:

啟動一個新的協程并不會掛起當前協程,只有當使用庫函數 await、yield方法時才會將當前的協程掛起。
協程掛起并不會阻塞線程,線程在掛起點 return 后可以去執行其他的代碼塊。
協程的掛起過程很簡單,代碼塊直接返回,當前狀態保存在狀態機 SuspendLambda 中,可以想象到協程恢復的時候也是調用 SuspendLambda 的 invokeSuspend 從而進入下一個狀態繼續執行的。

delay 的實現

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

/** Returns [Delay] implementation of the given context */
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

internal actual val DefaultDelay: Delay = DefaultExecutor

delay 使用suspendCancellableCoroutine掛起協程,而協程恢復的一般情況下是關鍵在DefaultExecutor.scheduleResumeAfterDelay(),其中實現是schedule(DelayedResumeTask(timeMillis, continuation)),其中的關鍵邏輯是將 DelayedResumeTask 放到 DefaultExecutor 的隊列最后,在延遲的時間到達就會執行 DelayedResumeTask,那么該 task 里面的實現是什么:

override fun run() {
    // 直接在調用者線程恢復協程
    with(cont) { resumeUndispatched(Unit) }
}

yield 的實現

yield()的作用是掛起當前協程,然后將協程分發到 Dispatcher 的隊列,這樣可以讓該協程所在線程或線程池可以運行其他協程邏輯,然后在 Dispatcher 空閑的時候繼續執行原來協程。簡單的來說就是讓出自己的執行權,給其他協程使用,當其他協程執行完成或也讓出執行權時,一開始的協程可以恢復繼續運行。
看下面的代碼示例:

fun main(args: Array<String>) = runBlocking<Unit> {
    launch {
        repeat(3) {
            println("job1 repeat $it times")
            yield()
        }
    }
    launch {
        repeat(3) {
            println("job2 repeat $it times")
            yield()
        }
    }
}

通過yield()實現 job1 和 job2 兩個協程交替運行,輸出如下:

job1 repeat 0 times
job2 repeat 0 times
job1 repeat 1 times
job2 repeat 1 times
job1 repeat 2 times
job2 repeat 2 times

現在來看其實現:

public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    val context = uCont.context
    // 檢測協程是否已經取消或者完成,如果是的話拋出 CancellationException
    context.checkCompletion()
    // 如果協程沒有線程調度器,或者像 Dispatchers.Unconfined 一樣沒有進行調度,則直接返回
    val cont = uCont.intercepted() as? DispatchedContinuation<Unit> ?: return@sc Unit
    if (!cont.dispatcher.isDispatchNeeded(context)) return@sc Unit
    // dispatchYield(Unit) 最終會調用到 dispatcher.dispatch(context, block) 將協程分發到調度器隊列中,這樣線程可以執行其他協程
    cont.dispatchYield(Unit)
    COROUTINE_SUSPENDED
}

所以注意到,yield()需要依賴協程的線程調度器,而調度器再次執行該協程時,會調用resume來恢復協程運行。

現在來看封裝異步邏輯為掛起函數的關鍵是用suspendCoroutineUninterceptedOrReturn函數包裝,然后在異步邏輯完成時調用resume手動恢復協程。

協程工作流程圖
image.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容