从底层聊一聊协程的实现原理

实现协程的核心:跳转(协程切换)

  • 协程想要拥有同步的编程方式和异步的性能,因此我们不能对同步的代码进行修改,而要想办法对异步的代码进行修改,使得其
  • 下面我们以https://blog.csdn.net/qq_41453285/article/details/106357786中的HTTP客户端异步实现代码为例
  • 下面且听我细细道来

如何跳转?往哪里跳转?

  • 在代码中,客户端调用async_http_commit()函数向服务端发送一个HTTP请求,为了实现异步的方式,我们在调用send()发送数据之后,把这个fd添加到epoll中进行管理(这是异步的方式),而不是在send()后面调用recv()(这是同步的方式)

  • 下面是async_http_thread_func()函数的代码,这个函数在while(1)循环中一直调用epoll_wait()监听描述符是否有事件发生,例如上面的async_http_commit()函数调用send()给服务端发送数据之后,服务端给客户端回送响应,那么epoll_wait()就会被触发,从而调用recv()接收数据

  • 那么如何用异步代码实现同步的效果呢?那就需要程序进行跳转在
  • 关于跳转可以详细见下面的解释(图片点开来看):
    • (图左侧)async_http_commit()函数接收完数据之后为了实现与同步一样的效果,我们跳转到右侧的async_http_thread_func()函数
    • (图右侧)跳转到async_http_thread_func()函数之后就可以调用epoll_wait()来检测数据了(因为上面的async_http_commit()函数调用了send()发送数据了),如果epoll_wait()检测到有数据来之后就可以在下面接收数据,当接收完数据之后再跳转回到左侧的async_http_commit()函数中

跳转的方法有哪些呢?

  • 程序跳转的方法有3种:
    • ①setjmp()、longjmp()函数:不推荐使用,编码复杂
    • ②ucontext
    • ③汇编实现
  • 这3种方法,其中最常用的就是汇编,并且Go等语言的协程也是用汇编实现的

yield、resume

  • yield:让出当前的CPU,跳转到指定的位置进行执行,这个过程叫做yield
  • resume:上面yield让出CPU之后跳转到指定的位置执行,当指定的位置执行完成之后,回到当初的位置这个过程叫做resume
  • 例如,对于上面来说,就是

六、如何通过汇编实现协程的切换呢?

  • 下载代码https://github.com/wangbojing/NtyCo开始解析,后面要用到
  • 对于上面的跳转来说,其实就是协程之间的切换,如何实现这种切换呢?
  • 在介绍协程切换之前,来说一下线程的切换,如下图所示:
    • CPU有很多的寄存器,这些寄存器保存了当先在处理器上运行线程的信息
    • 例如当前CPU运行的是A线程,那么寄存器保存的都是线程A的信息
    • 当此时需要把线程A切出CPU,来让B线程在CPU上运行,那么就需要把当前寄存器的内容都保存在线程A的栈中,然后把B运行所需要的内容加载到寄存器中,从而使得B线程在CPU上运行起来

  • 协程如何切换?与线程是相同的道理,还是以上面的图片为例:
    • sync_http_commit()函数调用send()之后,在跳转到pos位置之前把当前寄存器的内容保存到一个结构体中(例如命名为store结构体)
    • 跳转到async_http_thread_func()函数指定的pos位置之后,把pos位置的内容信息加载到寄存器中开始执行

如何实现这些内容的保存与切换

  • 首先到代码的Nty_coroutine.c文件中找到_switch()函数,这个函数是实现切换的核心:
    • 参数1:新的上下文
    • 参数2:当前的上下文
    • _switch()函数就是把当前寄存器的内容保存在参数2中,然后加载参数1所指定的内容加载到寄存器中
    • 例如,如果是上面的sync_http_commit()函数,其在"jump-pos"的时候就调用_switch()进行切换,然后async_http_thread_func()函数执行完需要切换回sync_http_commit()函数的时候,会在"jump->back"的地方调用这个函数,只是参数不同而已

  • nty_cpu_ctx结构体就是一些寄存器的指针

  • _switch()函数可以用下面的图来表示

  • 那么如何实现这些寄存器值的保存与交换呢?
    • 以下面为例,自己看图片吧,稍微有点复杂
    • _switch()函数的参数1名为rdi、参数2名为rsi
    • 在左侧,前一半部分:汇编指令把寄存器的内容保存到rsi中,留下次跳转回来使用;后一半部分:把rdi的内容加载到寄存器中开始使用
    • 0、8、16那些是偏移,因为一个指针就是8字节,所以rsp对应的是esp、rbp对应的是ebp......依次类推

  • X86-64有16个64位寄存器,分别是:
    • %rax:作为函数返回值使用
    • %rsp:栈指针寄存器,指向栈顶
    • %rdi,%rsi,%rdx,%rcx,%r8,%r9:用作函数参数,依次对应第1参数,第2参数......依次类推(例如在_switch()函数中,参数1叫做rdi、参数2叫做rsi......)
    • %rbx,%rbp,%r12,%r13,%14,%15:用作数据存储,遵循被调用者使用规则,简单说就是随便 用,调用子函数之前要备份它,以防他被修改 %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值