kotlin的suspend对比csharp的async&await
阅读原文时间:2023年07月09日阅读:2

协程的出现大大降低了异步编程的复杂度,可以让我们像写同步代码一样去写异步代码,如果没有它,那么很多异步的代码都是需要靠回调函数来一层层嵌套,这个在我之前的一篇有介绍 rxjava回调地狱-kotlin协程来帮忙

本篇文章主要介绍

  • kotlin的suspend函数在编译生成了怎样的代码

  • csharp的async&await在编译生成了怎么样的代码

  • 这两者相比较,引发怎样的思考

kotlin的suspend函数demo

image

这里针对kotlin的语法以及协程的具体用法细节不过多介绍,就当你已了解

稍微注意下runBlocking函数比较特别,

如下图:它接受了一个suspend的block函数

image

所以我上面的demo这里面有其实有三个suspend函数!

在idea我们可以把这个kotlin代码反编译成java代码

image

这个反编译后的java代码 有很多报错是无法直接copy出来运行的(这就没有csharp做的好,csharp反编译出来的代码至少不会报红),

image

看代码的确是一个状态机控制函数和一个匿名类,还原成正常的java代码如下:

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;

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

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

其他的函数也类似,完整的代码请查看:

https://gist.github.com/yuzd/cf67048777f0eb8fc1b3757f5bf9e8f3

整个运行流程如下:

kotlin协程的挂起点是怎么控制的,异步操作执行完后它知道从哪里恢复?

不难看出来suspend函数其实在编译后是变成了状态机,将我们顺序执行的代码,转换成了回调的形式 父suspend函数里面调用子suspend函数,其实是把自己传给了子suspend状态机,如果子函数挂起了,等子函数恢复后直接调用父函数(因为通过状态机的label来控制走不同逻辑,去恢复当时的调用堆栈)

这就是协程的挂起与恢复机制了

csharp的async&await

demo

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

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

&nbsp;}

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

我们反编译查看下编译器生成了怎样的状态机

image

看反编译的代码比较吃力,我还原成了正常代码,

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

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

完整代码请查看 https://github.com/yuzd/asyncawait_study

可以看出来,和kotlin其实原理差不多,都是生成一个函数加一个状态机

区别是csharp的函数就是创建一个状态机且启动它

//&nbsp;当状态机启动时会触发&nbsp;状态机的MoveNext方法的调用
stateMachine._builder.Start(ref&nbsp;stateMachine);

image

整体的执行流程如下

image

ps:最右边的是展示如果有多个await 那么就会对应这个状态机的多个状态

这两者相比较,引发怎样的思考

通过查看kotlin和csharp的实现方式,我发现kotlin的生成的状态机(ContinuationImpl的实现)都是有继承关系的, 比如demo中的test2继承了test1,test继承了main(通过构造函数传递的)

然而csharp中没有这样的关系

这也带来了两者最大的区别,kotlin的协程绑定了scope的概念,一旦scope被取消,那么scope绑定的所有的协程也都被取消。

这点好像在csharp中没有(如果理解有误欢迎指正)

这在实际应用中是怎么个区别呢,举个例子

async&nbsp;void&nbsp;testAsyncA(){
&nbsp;&nbsp;&nbsp;&nbsp;testAsyncB();
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;我想取消,或者下面运行出异常了&nbsp;我也无法取消testAsyncB这个任务
&nbsp;&nbsp;&nbsp;&nbsp;
}

async&nbsp;void&nbsp;testAsyncB(){
&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;do&nbsp;long&nbsp;task
}

在kotlin是可以的

image

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