美文网首页
从coobjc理解协程

从coobjc理解协程

作者: Boothlee | 来源:发表于2019-03-09 18:29 被阅读14次

    前言:

    自己对协程的概念的理解,源于coobjc的开源。文章参考了其他人对于协程的理解,加以融合贯通,希望能对不了解协程的人给予理解上的帮助。

    协程的概念可能很多人不熟悉,第一次听到这个词,可能是这样:

    什么是协程.jpg

    完整的漫画在这里。

    看完漫画,可能对协程有了初步的了解,至少知道除了订机票,还能编程~

    理解协程,要先明确一个概念:用户态线程

    引用一段话加以说明:

    • 一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部可以是多cpu并行,也可以是单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。
    • 但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
    • 后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行。
    • 因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪,资源分配也很难做到相对的公平。所以核心的操作需要陷入内核(kernel),切换到操作系统,让老大帮你来做。
    • 有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
    • 如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。
    • 从上面可以看到,实现一个用户态线程有两个必须要处理的问题:一是碰着阻塞式I\O会导致整个进程被挂起;二是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。

    了解了用户态线程,就对理解协程更近了一步

    协程它能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。普通过程(函数)可看成这个特殊过程的一个特例:只有一个状态,每次进入时局部状态重置。

    下面结合coobjc理解一下协程如何控制,切换状态:

    上面说的调用状态,我理解成线程的寄存器状态,协程的操作其实就是记录寄存器状态到对应的上下文中,继而通过上线文,控制协程的操作。在coobjc中对应的应该就是coroutine_ucontext_t
    对应的启动,切换控制权,恢复就是通过操作coroutine_ucontext_t,核心API如下
    coobjc 协程实现的核心在
    core/coroutine_context 中,点开头文件,可以看到

    //获取协程上下文
    extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
    //设置协程上下文
    extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
    //设置协程上下文
    extern int coroutine_begin (coroutine_ucontext_t *__ucp);
    //创建协程上下文
    extern void coroutine_makecontext (coroutine_ucontext_t *__ucp, IMP func, void *arg, void *stackTop);
    

    这里使用汇编实现了上下文的获取、设置:

    #if defined(__arm64__) || defined(__aarch64__)
    
    .text
    .align 2
    .global _coroutine_getcontext
    _coroutine_getcontext:
        stp    x18,x19, [x0, #0x090]
        stp    x20,x21, [x0, #0x0A0]
        stp    x22,x23, [x0, #0x0B0]
        stp    x24,x25, [x0, #0x0C0]
        stp    x26,x27, [x0, #0x0D0]
        str    x28, [x0, #0x0E0];
        stp    x29, x30, [x0, #0x0E8];  // fp, lr
        mov    x9,      sp
        str    x9,      [x0, #0x0F8]
        str    x30,     [x0, #0x100]    // store return address as pc
        stp    d8, d9,  [x0, #0x150]
        stp    d10,d11, [x0, #0x160]
        stp    d12,d13, [x0, #0x170]
        stp    d14,d15, [x0, #0x180]
        mov    x0, #0                   
        ret
    
    .global _coroutine_begin
    _coroutine_begin:
        ldp    x18,x19, [x0, #0x090]
        ldp    x20,x21, [x0, #0x0A0]
        ldp    x22,x23, [x0, #0x0B0]
        ldp    x24,x25, [x0, #0x0C0]
        ldp    x26,x27, [x0, #0x0D0]
        ldp    x28,x29, [x0, #0x0E0]
        ldr    x9,     [x0, #0x100]  // restore pc into lr
        mov    x30,   #0;
        ldr    x1,      [x0, #0x0F8]
        mov    sp,x1                  // restore sp
        ldp    d8, d9,  [x0, #0x150]
        ldp    d10,d11, [x0, #0x160]
        ldp    d12,d13, [x0, #0x170]
        ldp    d14,d15, [x0, #0x180]
        ldp    x0, x1,  [x0, #0x000]  // restore x0,x1
        ret    x9
    
    .global _coroutine_setcontext
    _coroutine_setcontext:
        ldp    x18,x19, [x0, #0x090]
        ldp    x20,x21, [x0, #0x0A0]
        ldp    x22,x23, [x0, #0x0B0]
        ldp    x24,x25, [x0, #0x0C0]
        ldp    x26,x27, [x0, #0x0D0]
        ldp    x28,x29, [x0, #0x0E0]
        ldr    x30,     [x0, #0x100]  // restore pc into lr
        ldr    x1,      [x0, #0x0F8]
        mov    sp,x1                  // restore sp
        ldp    d8, d9,  [x0, #0x150]
        ldp    d10,d11, [x0, #0x160]
        ldp    d12,d13, [x0, #0x170]
        ldp    d14,d15, [x0, #0x180]
        ldp    x0, x1,  [x0, #0x000]  // restore x0,x1
        ret    x30
    
    

    最后列举一点协程的优点

    • 跨平台
    • 跨体系架构
    • 无需线程上下文切换的开销
    • 无需原子操作锁定及同步的开销

    协程没有线程的切换,省去了切换线程需要的开销,这是协程对比多线程的优势。协程不是多线程,自然也舍去了加锁,解锁的操作。但是协程通过代码的控制逻辑,中断,恢复任务。

    参考:

    coobjc

    协程的好处有哪些?

    阿里开源 iOS 协程开发框架 coobjc源码分析

    相关文章

      网友评论

          本文标题:从coobjc理解协程

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