Kotlin协程原理

作者: 奔跑吧李博 | 来源:发表于2023-11-04 20:36 被阅读0次
    线程与协程关系:

    协程虽然不能脱离线程而运行,但可以在不同的线程之间切换。

    我为什么要用上协程呢?

    Kotlin 协程的核心竞争力在于:它能简化异步并发任务。

    比如需要做如下的一个功能:
    查询用户信息 --> 查找该用户的好友列表 -->拿到好友列表后,查找该好友的动态
    用地狱回调写法如下:

    
    getUserInfo(new CallBack() {
        @Override
        public void onSuccess(String user) {
            if (user != null) {
                System.out.println(user);
                getFriendList(user, new CallBack() {
                    @Override
                    public void onSuccess(String friendList) {
                        if (friendList != null) {
                            System.out.println(friendList);
                            getFeedList(friendList, new CallBack() {
                                @Override
                                public void onSuccess(String feed) {
                                    if (feed != null) {
                                        System.out.println(feed);
                                    }
                                }
                            });
                        }
                    }
                });
            }
        }
    });
    

    而使用协程的写法代码就是如此清晰:

    val user = getUserInfo()
    val friendList = getFriendList(user)
    val feedList = getFeedList(friendList)
    
    suspend fun getUserInfo(): String {
        withContext(Dispatchers.IO) {
            delay(1000L)
        }
        return "BoyCoder"
    }
    
    suspend fun getFriendList(user: String): String {
        withContext(Dispatchers.IO) {
            delay(1000L)
        }
        return "Tom, Jack"
    }
    
    suspend fun getFeedList(list: String): String {
        withContext(Dispatchers.IO) {
            delay(1000L)
        }
        return "{FeedList..}"
    }
    
    
    协程实现原理

    定义挂起函数

    suspend fun requestLoadUser(id: String)
    

    挂起函数spspend的作用:
    声明suspend函数,它是提醒开发者,这个方法里面是个耗时函数,如果你在主线程要调用,你得在协程中调用,并且切换到其它线程去调用,否则会卡顿主线程。

    原本需要异步执行并使用回调的写法,用同步的写法,用挂起和恢复进行实现。

    协程的挂起(suspend)和恢复(resume):

    Kotlin 的编译器检测到 suspend 关键字修饰的函数以后,会自动将挂起函数转换成带有回调的函数。
    反编译之后:

    //                              Continuation 等价于 CallBack
    //                                         ↓         
    public static final Object getUserInfo(Continuation $completion) {
      ...
      return "BoyCoder";
    
    

    可以看到,多了个Continuation(继续)参数,这是个接口,是在本次函数执行完毕后执行的回调。

    Continuation的代码:

    public interface Continuation<in T> {
        /**
         * 保存上下文(比如变量状态)
         */
        public val context: CoroutineContext
        /**
         * 方法执行结束的回调,参数是个泛型,用来传递方法执行的结果
         */
        public fun resumeWith(result: Result<T>)
    }
    

    suspend代码示例:

    suspend fun getToken(id: String): String = "token"
    suspend fun getInfo(token: String): String = "info"
    
    // 添加了局部变量a,看下suspend怎么保存a这个变量
    suspend fun test() {
        val token = getToken("123") // 挂起点1,这里是异步线程
        var a = 10 // 这里是10  //主线程
        val info = getInfo(token) // 挂起点2,需要将前面的数据保存(比如a),在挂起点之后恢复   //异步线程
        println(info)  //主线程
        println(a
    }
    

    反编译之后:

    public final Object getToken(String id, Continuation completion) {
        return "token";
    }
    
    public final Object getInfo(String token, Continuation completion) {
        return "info";
    }
    
    // 重点函数(伪代码)
    public final Object test(Continuation<String>: continuation) {
        Continuation cont = new ContinuationImpl(continuation) {
            int label; // 保存状态
            Object result; // 保存中间结果,还记得那个Result<T>吗,是个泛型,因为泛型擦除,所以为Object,用到就强转
            int tempA; // 保存上下文a的值,这个是根据具体代码产生的
        };
        switch(cont.label) {
            case 0 : {
                cont.label = 1; //更新label
                
                getToken("123",cont) // 执行对应的操作,注意cont,就是传入的回调
                break;
            }
    
            case 1 : {
                cont.label = 2; // 更新label
                
                // 这是一个挂起点,我们要保存上下文数据,这里就保存a的值
                int a  = 10;
                cont.tempA = a; // 保存a的值 
    
                // 获取上一步的结果,因为泛型擦除,需要强转
                String token = (Object)cont.result;
                getInfo(token, cont); // 执行对应的操作
                break;
            }
    
            case 2 : {
                String info = (Object)cont.result; // 获取上一步的结果
                println(info); // 执行对应的操作
    
                // 在挂起点之后,恢复a的值
                int a = cont.tempA;
                println(a);
    
                return;
            }
        }
    }
    

    我们可以将每个case理解为一个状态,每个case分支对应的语句,理解为一个Continuation实现。
    上述伪代码大致描述了协程的调度流程:

    1 调用test函数时,需要传入一个Continuation接口,我们会对它进行二次装饰。
    2 装饰就是根据函数具体逻辑,在内部添加额外的上下文数据和状态信息(也就是label)。
    3 每个状态对应一个Continuation接口,里面会执行对应的业务逻辑。
    4 每个状态都会: 保存上下文信息 -> 获取上一个状态的结果 -> 执行本状态业务逻辑 -> 恢复上下文信息。
    5 直到最后一个状态对应的逻辑执行完毕。

    总结:

    1 Kotlin中,每个suspend方法,都需要一个Continuation接口实现,用来执行下一个状态的操作;并且,每个suspend方法的调用点都会产生一个挂起点。
    2 每个挂起点,都会产生一个label,对应于状态机的一个状态,不同的状态之间,通过Continuation来切换。
    3 Kotlin协程会在每个挂起点保存当前的上下文数据,并且在挂起点之后进行恢复。这样,每个状态之间就是相互独立的,可以独立调度。
    4 协程的切换,只不过是从一种状态切换到另一种状态,因为不同状态是相互独立的,所以在合适的时机,再切换回来也不会对结果造成影响。

    参考:
    https://www.modb.pro/db/211852
    https://mp.weixin.qq.com/s/70wBBKwFFLb0X_zrsvNzDA
    https://blog.csdn.net/jinking01/article/details/130520579

    https://www.bilibili.com/video/BV1KJ41137E9/?spm_id_from=333.337.search-card.all.click&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

    相关文章

      网友评论

        本文标题:Kotlin协程原理

        本文链接:https://www.haomeiwen.com/subject/ypgaidtx.html