kotlin的suspend對比csharp的async&await

協程的出現大大降低了非同步編程的複雜度,可以讓我們像寫同步程式碼一樣去寫非同步程式碼,如果沒有它,那麼很多非同步的程式碼都是需要靠回調函數來一層層嵌套,這個在我之前的一篇有介紹 rxjava回調地獄-kotlin協程來幫忙

本篇文章主要介紹

  • kotlin的suspend函數在編譯生成了怎樣的程式碼
  • csharp的async&await在編譯生成了怎麼樣的程式碼
  • 這兩者相比較,引發怎樣的思考

kotlin的suspend函數demo

image
image

這裡針對kotlin的語法以及協程的具體用法細節不過多介紹,就當你已了解

稍微注意下runBlocking函數比較特別,

如下圖:它接受了一個suspend的block函數

image
image

所以我上面的demo這裡面有其實有三個suspend函數!

在idea我們可以把這個kotlin程式碼反編譯成java程式碼

image
image

這個反編譯後的java程式碼 有很多報錯是無法直接copy出來運行的(這就沒有csharp做的好,csharp反編譯出來的程式碼至少不會報紅),

image
image

看程式碼的確是一個狀態機控制函數和一個匿名類,還原成正常的java程式碼如下:

image
image

比如test1函數


public static Object test1(Continuation continuation) {
    CoroutineTest1 continuationTest1;
    label20:
    {
        if (continuation instanceof CoroutineTest1) {
            continuationTest1 = (CoroutineTest1) continuation;
            int i = continuationTest1.label & Integer.MIN_VALUE;
            if (i != 0) {
                continuationTest1.label -= Integer.MIN_VALUE;
            }
            break label20;
        }
        continuationTest1 = new CoroutineTest1(continuation);
    }

    Object result = (continuationTest1).result;
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    String var1;
    switch ((continuationTest1).label) {
        case 0:
            ResultKt.throwOnFailure(result);
            var1 = "test1-start";
            System.out.println(var1);
            (continuationTest1).label = 1;
            if (test2(continuationTest1) == var4) {
                return var4;
            }
            break;
        case 1:
            ResultKt.throwOnFailure(result);
            break;
        default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }

    var1 = "test1-end";
    System.out.println(var1);
    return Unit.INSTANCE;
}

final static class CoroutineTest1 extends ContinuationImpl {
    Object result;
    int label;

    public CoroutineTest1(@Nullable Continuation<Object> completion) {
        super(completion);
    }

    @Nullable
    public Object invokeSuspend(@NotNull Object $result) {
        this.result = $result;
        this.label |= Integer.MIN_VALUE;
        return test1(this);
    }
}

其他的函數也類似,完整的程式碼請查看:

//gist.github.com/yuzd/cf67048777f0eb8fc1b3757f5bf9e8f3

整個運行流程如下: image

kotlin協程的掛起點是怎麼控制的,非同步操作執行完後它知道從哪裡恢復?

不難看出來suspend函數其實在編譯後是變成了狀態機,將我們順序執行的程式碼,轉換成了回調的形式 父suspend函數裡面調用子suspend函數,其實是把自己傳給了子suspend狀態機,如果子函數掛起了,等子函數恢復後直接調用父函數(因為通過狀態機的label來控制走不同邏輯,去恢復當時的調用堆棧)

這就是協程的掛起與恢復機制了

csharp的async&await

demo

static async Task Main(string[] args)
{
   await test1();      
   Console.WriteLine("Let's Go!");
}

async Task test1(){
  Console.WriteLine("test1-start");
  await test2();
  Console.WriteLine("test1-end");

 }

async Task test2()
{
  Console.WriteLine("test2-start");
  await Task.Delay(1000);
  Console.WriteLine("test2-end");
 }

我們反編譯查看下編譯器生成了怎樣的狀態機

image
image

看反編譯的程式碼比較吃力,我還原成了正常程式碼,

static Task CreateMainAsyncStateMachine()
{
 MainAsyncStateMachine stateMachine = new MainAsyncStateMachine
 {
  _builder = AsyncTaskMethodBuilder.Create(),
  _state = -1
 };
 stateMachine._builder.Start(ref stateMachine);
 return stateMachine._builder.Task;
}

struct MainAsyncStateMachine : IAsyncStateMachine
{
 public int _state;
 public AsyncTaskMethodBuilder _builder;
 public TaskAwaiter _waiter;
 public void MoveNext()
 {
  int num1 = this._state;
  try
  {
   TaskAwaiter awaiter;
   int num2;
   if (num1 != 0)
   {
    awaiter = UserQuery.CreateTest1AsyncStateMachine().GetAwaiter();
    if (!awaiter.IsCompleted)
    {
     Console.WriteLine("MainAsyncStateMachine######Test1AsyncStateMachine IsCompleted:false, 註冊自己到Test1Async運行結束時運行");
     this._state = num2 = 0;
     this._waiter = awaiter;
     this._builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
     return;
    }
   }
   else
   {
    Console.WriteLine("MainAsyncStateMachine######Test1AsyncStateMachine IsCompleted:true");
    awaiter = this._waiter;
    this._waiter = new TaskAwaiter();
    this._state = num2 = -1;
   }
   awaiter.GetResult();
   Console.WriteLine("MainAsyncStateMachine######Let's Go!");
  }
  catch (Exception e)
  {
   this._state = -2;
   this._builder.SetException(e);
   return;
  }
  this._state = -2;
  this._builder.SetResult();
 }
 public void SetStateMachine(IAsyncStateMachine stateMachine)
 {
  this._builder.SetStateMachine(stateMachine);
 }
}

完整程式碼請查看 //github.com/yuzd/asyncawait_study

可以看出來,和kotlin其實原理差不多,都是生成一個函數加一個狀態機

區別是csharp的函數就是創建一個狀態機且啟動它

// 當狀態機啟動時會觸發 狀態機的MoveNext方法的調用
stateMachine._builder.Start(ref stateMachine);
image
image

整體的執行流程如下

image
image

ps:最右邊的是展示如果有多個await 那麼就會對應這個狀態機的多個狀態

這兩者相比較,引發怎樣的思考

通過查看kotlin和csharp的實現方式,我發現kotlin的生成的狀態機(ContinuationImpl的實現)都是有繼承關係的, 比如demo中的test2繼承了test1,test繼承了main(通過構造函數傳遞的)

然而csharp中沒有這樣的關係

這也帶來了兩者最大的區別,kotlin的協程綁定了scope的概念,一旦scope被取消,那麼scope綁定的所有的協程也都被取消。

這點好像在csharp中沒有(如果理解有誤歡迎指正)

這在實際應用中是怎麼個區別呢,舉個例子

async void testAsyncA(){
    testAsyncB();
    
    // 我想取消,或者下面運行出異常了 我也無法取消testAsyncB這個任務
    
}

async void testAsyncB(){
    // do long task
}

在kotlin是可以的

image
image

suspend fun test2() = coroutineScope {
    println("test2-start")
    async {
        delay(100000);
    }
    delay(1000)
    println("test2-end")
    // 或者手動取消當前coroutineScope
    this.cancel()
}