Kotlin协程在内部如何工作?

时间:2018-11-28 19:15:03

标签: kotlin kotlin-coroutines

Kotlin如何在内部实现协程?

协程被称为线程的“较轻版本”,我了解它们在内部使用线程来执行协程。

使用任何构建器功能启动协程时会发生什么?

这是我对运行此代码的理解:

/**
 * module.cpp
 */

// Helper class, not visible outside this file.
// Invoked like a function.
class help {
public:
  help() { step1(); step2(); step3(); }
private:
  void step1();
  void step2();
  void step3();

  int a;
  ComplexType b;
  vector<ComplexType2> c;
};

// Implementation of help...

// Implementation of the public function
void io_sequence()
{
  (void) help();
}
  1. 科特琳开头有一个预定义的GlobalScope.launch { <---- (A) val y = loadData() <---- (B) // suspend fun loadData() println(y) <---- (C) delay(1000) <---- (D) println("completed") <---- (E) }
  2. ThreadPool,Kotlin在下一个可用的空闲线程(说(A))中开始执行协程。
  3. Thread01,Kotlin停止执行当前线程,并在下一个可用的空闲线程((B))中启动挂起函数loadData()
  4. 执行后返回Thread02时,Kotlin在下一个可用的空闲线程中继续协程 (B))。
  5. Thread03(C)上执行。
  6. Thread03(D)停止了。
  7. 1000毫秒后,Thread03在下一个空闲线程(例如(E))上执行。

我理解正确吗?还是协程以不同的方式实现?

2 个答案:

答案 0 :(得分:7)

协程与您描述的任何调度策略完全不同。协程基本上是suspend fun的调用链。暂停完全在您的控制之下:您只需致电suspendCoroutine。您将获得一个回调对象,因此可以调用其resume方法并返回到挂起的位置。

在某些代码中,您可以看到暂停是一种非常直接和透明的机制,完全在您的控制之下:

import kotlin.coroutines.*
import kotlinx.coroutines.*

var continuation: Continuation<String>? = null

fun main(args: Array<String>) {
    val job = GlobalScope.launch(Dispatchers.Unconfined) {
        while (true) {
            println(suspendHere())
        }
    }
    continuation!!.resume("Resumed first time")
    continuation!!.resume("Resumed second time")
}

suspend fun suspendHere() = suspendCancellableCoroutine<String> {
    continuation = it
}

launch的协程在每次调用suspendHere()时都会自行挂起。它将继续回调写入continuation属性,然后您显式地使用该继续来恢复协程。

代码使用Unconfined协程分派器,它根本不分派线程,它只是在您调用continuation.resume()的地方运行协程代码。


考虑到这一点,让我们重新查看一下图:

GlobalScope.launch {       <---- (A)
    val y = loadData()     <---- (B)  // suspend fun loadData() 
    println(y)             <---- (C)
    delay(1000)            <---- (D)
    println("completed")   <---- (E)
}
  
      
  1. 科特琳开头有一个预定义的ThreadPool
  2.   

它可能有也可能没有线程池。 UI调度程序使用单个线程。

使线程成为协程调度程序的目标的先决条件是,存在与之关联的并发队列,并且该线程运行一个顶级循环,该循环从此队列中获取Runnable个对象并执行它们。协程调度员只需将继续操作放在该队列中即可。

  
      
  1. (A),Kotlin在下一个可用的空闲线程(说Thread01)中开始执行协程。
  2.   

它也可以与您调用launch的线程相同。

  
      
  1. (B),Kotlin停止执行当前线程,并在下一个可用的空闲线程(loadData())中启动挂起函数Thread02
  2.   

Kotlin无需停止任何线程即可挂起协程。实际上,协程的主要目的是不要启动或停止线程。线程的顶级循环将继续并选择另一个可运行的线程。

此外,仅在呼叫suspend fun的事实就没有任何意义。协程仅在显式调用suspendCoroutine时才会挂起自身。该函数也可以简单地返回而不会暂停。

但是假设它确实调用了suspendCoroutine。在这种情况下,协程不再在任何线程上运行 。它已被挂起,直到某些代码在某处调用continuation.resume()之前无法继续。该代码可以在将来的任何时间在任何线程上运行。

  
      
  1. 执行后返回(B)时,Kotlin在下一个可用的空闲线程中继续协程 Thread03)。
  2.   

B不会“执行后返回”,协程仍在其体内时恢复。返回之前,它可能会暂停并恢复任意次数。

  
      
  1. (C)Thread03上执行。
  2.   
  3. (D)Thread03停止了。
  4.   
  5. 1000毫秒后,(E)在下一个空闲线程(例如Thread01)上执行。
  6.   

同样,没有线程被停止。协程被挂起,通常针对调度程序的机制用于调度1000 ms之后的恢复。届时它将被添加到与调度程序关联的运行队列中。


为具体起见,让我们来看一些示例,这些示例说明了派出协程使用哪种代码。

Swing UI调度程序:

EventQueue.invokeLater { continuation.resume(value) }

Android UI调度程序:

mainHandler.post { continuation.resume(value) }

ExecutorService调度程序:

executor.submit { continuation.resume(value) } 

答案 1 :(得分:4)

协程通过在可能的恢复点之间进行切换来工作:

class MyClass$Coroutine extends CoroutineImpl {
    public Object doResume(Object o, Throwable t) {
        switch(super.state) {
        default:
                throw new IllegalStateException("call to \"resume\" before \"invoke\" with coroutine");
        case 0:  {
             // code before first suspension
             state = 1; // or something else depending on your branching
             break;
        }
        case 1: {
            ...
        }
        }
        return null;
    }
}

执行此协程的结果代码随后将创建该实例,并在每次需要恢复执行时调用doResume()函数,其处理方式取决于用于执行的调度程序。

这是一个简单的协程的示例编译:

launch {
    println("Before")
    delay(1000)
    println("After")
}

哪个会编译为此字节码

private kotlinx.coroutines.experimental.CoroutineScope p$;

public final java.lang.Object doResume(java.lang.Object, java.lang.Throwable);
Code:
   0: invokestatic  #18                 // Method kotlin/coroutines/experimental/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object;
   3: astore        5
   5: aload_0
   6: getfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
   9: tableswitch   { // 0 to 1
                 0: 32
                 1: 77
           default: 102
      }
  32: aload_2
  33: dup
  34: ifnull        38
  37: athrow
  38: pop
  39: aload_0
  40: getfield      #24                 // Field p$:Lkotlinx/coroutines/experimental/CoroutineScope;
  43: astore_3
  44: ldc           #26                 // String Before
  46: astore        4
  48: getstatic     #32                 // Field java/lang/System.out:Ljava/io/PrintStream;
  51: aload         4
  53: invokevirtual #38                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  56: sipush        1000
  59: aload_0
  60: aload_0
  61: iconst_1
  62: putfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
  65: invokestatic  #44                 // Method kotlinx/coroutines/experimental/DelayKt.delay:(ILkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
  68: dup
  69: aload         5
  71: if_acmpne     85
  74: aload         5
  76: areturn
  77: aload_2
  78: dup
  79: ifnull        83
  82: athrow
  83: pop
  84: aload_1
  85: pop
  86: ldc           #46                 // String After
  88: astore        4
  90: getstatic     #32                 // Field java/lang/System.out:Ljava/io/PrintStream;
  93: aload         4
  95: invokevirtual #38                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
  98: getstatic     #52                 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
 101: areturn
 102: new           #54                 // class java/lang/IllegalStateException
 105: dup
 106: ldc           #56                 // String call to \'resume\' before \'invoke\' with coroutine
 108: invokespecial #60                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
 111: athrow

我使用kotlinc 1.2.41对此进行了编译

从32到76是打印Before并调用挂起的delay(1000)的代码。

从77到101是用于打印After的代码。

从102到111是错误恢复状态的错误处理,如切换表中的default标签所示。

因此,作为总结,kotlin中的协程仅仅是由某些调度程序控制的状态机。