ntyco协程的理解

简介: ntyco协程的理解

早在很久之前第一份工作,就接触使用过协程,对它只有只可理解不可言说的概念,这里通过对ntyco协程代码的理解,梳理一下自己对协程的理解。

1:协程的概念

总是把协程俗称为用户态线程,我的理解是,协程依托于线程上执行子例程,由用户态把控协程的调度及上下文。

2:协程的性能

协程的性能差不多和线程池的性能相同,差不到哪里去(多线程有线程调度切换开销,协程有管理节点的内存消耗)

总结:协程同步的变成方式,可以达到异步的性能。

3:什么时候使用协程

百科上说:适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。

我想了想,这个使用场景其实还是没有明确的理解:能使用协程解决的问题,一般都可用多线程线程池的方式去替代。

协程可以处理一些上下文相关的io任务,不用关心同步资源共享的问题,处理IO密集型任务,对计算机cpu消耗不高的任务。

协程可以配合网络IO一起使用,IO的底层控制其实是epoll,所以性能上的提升应该也有限制。

4:ntyco中协程的实现

1:协程实现关注的技术点:

1:协程的结构如何定义。

2:协程的内存如何存储,初始化寄存器。

3:协程的如何调度切换。

4:如果管理协程状态调度节点,协程的使用逻辑(模拟文件描述符用epoll管理)。

2:具体分析协程的实现

1:概述:从业务流程思考

1:创建协程

2:任务的执行,调用回调函数和函数参数

3:多个协程,多个回调函数之间的切换。 ==》保存上一个协程的信息,和当前执行的协程,实现切换

4:协程的销毁,资源释放。

2:协程的结构

1:协程的节点:

寄存器中保存节点正在执行函数的上下文,执行的回调函数,函数参数

依托于线程,要保存线程栈空间的大小,栈地址

其他:协程的创建时间戳,协程的id,函数名,网络io处理相关等。

typedef void(* proc_coroutine)(void*);
typedef struct _nty_coroutine{
  nty_cpu_ctx ctx;        //寄存器中的信息
  proc_coroutine * func;  //真正执行的回调函数
  void * arg;       //回调函数对应的参数
  void *stack;      //线程栈的管理
  size_t stack_size;      
  size_t last_stack_size;
  uint64_t birth;     //协程的创建时间,一些标识
  uint64_t id;
  char funcname[64];
  uint32_t ops;        //正在运行标志
  nty_coroutine_status status; //协程当前的状态
  struct _nty_schedule *sched;    //协程的管理节点
}nty_coroutine;
1.1 寄存器中保存上下文关系的结构定义: (由汇编代码实现寄存器的切换,实现协程的切换)
//寄存器中保存的cpu中的设置:
typedef struct _nty_cpu_ctx {
  void *esp; //
  void *ebp;
  void *eip;
  void *edi;
  void *esi;
  void *ebx;
  void *r1;
  void *r2;
  void *r3;
  void *r4;
  void *r5;
} nty_cpu_ctx;
2:协程的管理节点:

协程的状态: 就绪,等待,睡眠,正在执行

用两个红黑树管理睡眠和等待的协程,用队列管理准备就绪的协程,用链表管理正在执行的协程(其实一个元素就够了)

其他管理:创建时间,个数,协程切换时间,内存地址,页大小(管理内存)

保存正在执行的寄存器上下文,执行中。 ==》其实也就是当前协程

//协程的管理节点
typedef struct _nty_schedule{
  nty_cpu_ctx ctx;          //最近一次resume的协程的内容
  nty_coroutine *curr_thread;     //当前执行的协程     ==》配合控制协程的切换
  void *  stack;
  size_t  stack_size;          //每个协程分配的内存的大小
  int   page_size;                   //内存分页的大小   与栈内存做对比处理
  uint64_t  birth;
  int     spawned_coroutines;
  uint64_t  default_timeout;
  //epoll处理 用eventfd 文件描述符的形式管理了红黑树节点
  int poller_fd;
  int eventfd;              //好像没有用到该节点   不用保存加入epoll中的fd
  struct epoll_event eventlist[NTY_CO_MAX_EVENTS];  //触发事件
  int num_new_events;                     //触发事件个数
  //用到的相关结构红黑树:
  nty_coroutine_rbtree_sleep sleeping;
  nty_coroutine_rbtree_wait waiting;
  //用到的相关队列
  nty_coroutine_queue ready;
  //管理正在执行的链表
  nty_coroutine_link busy;
}nty_schedule;

3:协程的创建及初始化

1:初始化管理节点 nty_schedule,一组协程节点管理只需要一个

在协程节点中实际触发。

pthread_key_create,pthread_setspecific和pthread_getpecific实现线程中不同的函数共享数据的方式,并设置资源的释放 ==》这里可以用函数的返回值返回结构形式代替,自己管理释放。

2:协程节点的初始化。 ==》为协程分配内存,处理寄存器节点执向该内存。

创建一个协程节点,申请内存,放入就绪队列中等待触发。

使寄存器节点中信息指向该内存。 ==》这里的内存与寄存器还没有关联,需要做一定的处理

ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。
esp:寄存器存放当前线程的栈顶指针
ebp:寄存器存放当前线程的栈底指针
eip:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
3:触发执行
1:主要的控制调度的两个函数,内部都是同样的线程切换逻辑。
void nty_coroutine_yield(nty_coroutine *co); //该协程睡眠     修改运行标志,切换下一个运行
  //调用switch函数进行切换
int nty_coroutine_resume(nty_coroutine *co); //恢复协程的执行  根据标志,进行初始化寄存器位置,切换执行,资源释放。  根据页管理内存
  //resume对没有执行过的协程的处理 初始化寄存器位置
    //resume对已经执行过的协程的处理 直接调用switch函数进行切换
2:核心模块:汇编实现的协程切换(切换寄存器中的信息)

操作的结构:实际上就是每个协程节点中保存的寄存器信息。

与当前寄存器中的信息进行交换处理:

x86_64 的寄存器有16个64位寄存器,分别是:%rax, %rbx, %rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15。
%rax 作为函数返回值使用的。
%rsp 栈指针寄存器,指向栈顶
%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话说,就是随便用。调用子函数之前要备份它,以防它被修改
%r10, %r11 用作数据存储,就是使用前要先保存原值。
//实现两部分功能  先保存寄存器中的值,再恢复到寄存器中  
//第一个参数要运行的协程, 第二个为正在运行的协程
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
__asm__ (
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);
按照x86_64的寄存器定义,
    %rdi保存第一个参数的值,即new_ctx的值,
    %rsi保存第二个参数的值,即保存cur_ctx的值。
    X86_64每个寄存器是64bit,8byte。
Movq %rsp, 0(%rsi) 保存在栈指针到cur_ctx实例的rsp项
Movq %rbp, 8(%rsi)
Movq (%rsp), %rax #将栈顶地址里面的值存储到rax寄存器中。Ret后出栈,执行栈顶
Movq %rbp, 8(%rsi) #后续的指令都是用来保存CPU的寄存器到new_ctx的每一项中
Movq 8(%rdi), %rbp #将new_ctx的值
Movq 16(%rdi), %rax #将指令指针rip的值存储到rax中
Movq %rax, (%rsp) # 将存储的rip值的rax寄存器赋值给栈指针的地址的值。
Ret # 出栈,回到栈指针,执行rip指向的指令。
3:nty_coroutine_yield 和nty_coroutine_resume函数细节

只有这两个函数调用协程的切换,控制了协程的调度。

nty_coroutine_yield :保存寄存器中的值,让出cpu,执行到最近的resume

==》这里应该获取一个协程,然后进行切换的。

nty_coroutine_resume:第一次的初始化,协程中初始化寄存器信息,切换cpu,执行该协程。

整个调度控制由nty_coroutine_resume+协程数据结构控制。

4:调度控制 nty_schedule_run

1:判断管理节点中各数据结构中是否有相关的任务。

2:处理睡眠到时的协程,进行触发执行,红黑树结构

3:处理准备就绪的协程,进行触发执行,队列结构。

4:处理有事件触发的协程,进行触发执行,红黑树结构。 ==》多于网络io配合

nty_coroutine_resume 函数触发的执行。

5:资源的销毁:

1:协程节点的资源销毁:根据状态调度销毁函数。

2:管理节点的资源销毁:通过pthread_once函数进行设置,销毁堆内存。

4:协程与网络IO适配使用

ntyco中对网络通信相关的api做了相关的处理,实现了一套以协程为机制的套接字处理方案。

主要函数如下:

其实主要业务逻辑就是把本次的fd进行加入epoll中,并管理自己的红黑树节点加入

处理已经触发的一个业务。

删除已经触发的业务的epoll监听和红黑树管理

static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {
  if (timeout == 0)
  {
    return poll(fds, nfds, timeout);   //这里没有用到poll的相关处理
  }
  if (timeout < 0)
  {
    timeout = INT_MAX;
  }
  nty_schedule *sched = nty_coroutine_get_sched();
  nty_coroutine *co = sched->curr_thread;
  int i = 0;
  for (i = 0;i < nfds;i ++) {
    struct epoll_event ev;
    ev.events = nty_pollevent_2epoll(fds[i].events);   //与poll兼容,把事件转为epoll
    ev.data.fd = fds[i].fd;
    epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);  //fd加入epoll中
    co->events = fds[i].events;
    nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout); //把fd与协程关联,并加入红黑树中
  }
    //特别注意,这个很关键,虽然参数没变,但是其实内部已经切换了下一个协程,最近resume的协程
  nty_coroutine_yield(co); 
  for (i = 0;i < nfds;i ++) {
    struct epoll_event ev;
    ev.events = nty_pollevent_2epoll(fds[i].events);
    ev.data.fd = fds[i].fd;
    epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);  //已经执行了这个协程事件,所以在epoll中移除该节点
    nty_schedule_desched_wait(fds[i].fd);  //从事件触发wait红黑树中删除该协程
  }
  return nfds;
}

5:扩展及相关demo

扩展:可以使用多线程,多进程(绑核)的方式实现多协程。

ntyco中的demo运行可以适配理解协程的实现逻辑,尤其是网络io相关的demo。

目录
相关文章
|
9天前
|
前端开发 编译器 Linux
浅谈C++20 协程那点事儿
本文是 C++20 的协程入门文章,作者围绕协程的概念到协程的实现思路全方位进行讲解,努力让本文成为全网最好理解的「C++20 协程」原理解析文章。
|
2月前
|
前端开发 程序员 调度
探索协程在 C++ 中的实现方式
探索协程在 C++ 中的实现方式
105 2
|
8月前
|
数据采集 缓存 调度
协程小练习
协程小练习
|
4月前
|
C++
C/C++协程学习笔记
C/C++协程学习笔记
|
4月前
|
存储 前端开发 rax
协程学习笔记
协程学习笔记
35 0
|
4月前
|
存储 前端开发 rax
协程学习笔记 NtyCo/libgo
协程学习笔记 NtyCo/libgo
33 0
|
11月前
|
数据采集 调度 开发者
协程这么好,那它能完全代替线程吗?
协程这么好,那它能完全代替线程吗?
119 0
协程这么好,那它能完全代替线程吗?
|
11月前
|
安全
c++20 协程(coroutine)
c++20 协程(coroutine)
172 0
|
网络协议 程序员 调度
使用协程实现多线程
使用协程实现多线程
198 0
|
Python
协程基础
协程基础
142 0