协程如何使用?与线程使用有何区别?

协程如何使用?与线程使用有何区别?,第1张

协程如何使用?与线程使用有何区别? 协程如何使用?与线程使用有何区别?

在做网络IO编程的时候,有一个非常理想的情况,就是每次accept返回的时候,就为新来的客户端分配一个线程,这样一个客户端对应一个线程。就不会有多个线程共用一个sockfd。每请求每线程的方式,并且代码逻辑非常易读。但是这只是理想,线程创建代价,调度代价就呵呵了。

先来看一下每请求每线程的代码如下:

while(1) {
	socklen_t len = sizeof(struct sockaddr_in);
    int clientfd = accept(sockfd, (struct sockaddr*)&remote, &len);

    pthread_t thread_id;
    pthread_create(&thread_id, NULL, client_cb, &clientfd);

}

这样的做法,写完放到生产环境下面,如果你的老板不打死你,你来找我。我来帮你老板,为民除害。

如果我们有协程,我们就可以这样实现。参考代码如下:
https://github.com/wangbojing/NtyCo/blob/master/nty_server_test.c

while (1) {
    socklen_t len = sizeof(struct sockaddr_in);
    int cli_fd = nty_accept(fd, (struct sockaddr*)&remote, &len);
        
    nty_coroutine *read_co;
    nty_coroutine_create(&read_co, server_reader, &cli_fd);

}

这样的代码是完全可以放在生成环境下面的。如果你的老板要打死你,你来找我,我帮你把你老板打死,为民除害。

线程的API思维来使用协程,函数调用的性能来测试协程。

NtyCo封装出来了若干接口,一类是协程本身的,二类是posix的异步封装
协程API:while

1. 协程创建
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg)
2. 协程调度器的运行
void nty_schedule_run(void)

POSIX异步封装API:

int nty_socket(int domain, int type, int protocol)
int nty_accept(int fd, struct sockaddr *addr, socklen_t *len)
int nty_recv(int fd, void *buf, int length)
int nty_send(int fd, const void *buf, int length)
int nty_close(int fd)

接口格式与POSIX标准的函数定义一致。

协程内部是如何工作呢?

先来看一下协程服务器案例的代码, 代码参考:https://github.com/wangbojing/NtyCo/blob/master/nty_server_test.c
分别讨论三个协程的比较晦涩的工作流程。第一个协程的创建;第二个IO异步 *** 作;第三个协程子过程回调

创建协程

当我们需要异步调用的时候,我们会创建一个协程。比如accept返回一个新的sockfd,创建一个客户端处理的子过程。再比如需要监听多个端口的时候,创建一个server的子过程,这样多个端口同时工作的,是符合微服务的架构的。

创建协程的时候,进行了如何的工作?创建API如下:

int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg)

参数1:nty_coroutine **new_co,需要传入空的协程的对象,这个对象是由内部创建的,并且在函数返回的时候,会返回一个内部创建的协程对象。

参数2:proc_coroutine func,协程的子过程。当协程被调度的时候,就会执行该函数。

参数3:void *arg,需要传入到新协程中的参数。

协程不存在亲属关系,都是一致的调度关系,接受调度器的调度。调用create API就会创建一个新协程,新协程就会加入到调度器的就绪队列中。

创建的协程具体步骤会在《协程的实现之原语 *** 作》来描述。

实现IO异步 *** 作

大部分的朋友会关心IO异步 *** 作如何实现,在send与recv调用的时候,如何实现异步 *** 作的。

先来看一下一段代码:

while (1) {
    int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);

    for (i = 0;i < nready;i ++) {

        int sockfd = events[i].data.fd;
        if (sockfd == listenfd) {
            int connfd = accept(listenfd, xxx, xxxx);
            
            setnonblock(connfd);

            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

        } else {
            
            epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
            recv(sockfd, buffer, length, 0);

            //parser_proto(buffer, length);

            send(sockfd, buffer, length, 0);
            epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, NULL);
        }
    }
}

在进行IO *** 作(recv,send)之前,先执行了 epoll_ctl的del *** 作,将相应的sockfd从epfd中删除掉,在执行完IO *** 作(recv,send)再进行epoll_ctl的add的动作。这段代码看起来似乎好像没有什么作用。

如果是在多个上下文中,这样的做法就很有意义了。能够保证sockfd只在一个上下文中能够 *** 作IO的。不会出现在多个上下文同时对一个IO进行 *** 作的。协程的IO异步 *** 作正式是采用此模式进行的。

把单一协程的工作与调度器的工作的划分清楚,先引入两个原语 *** 作 resume,yield会在《协程的实现之原语 *** 作》来讲解协程所有原语 *** 作的实现,yield就是让出运行,resume就是恢复运行。调度器与协程的上下文切换如下图所示
在协程的上下文IO异步 *** 作(nty_recv,nty_send)函数,步骤如下:

  1. 将sockfd 添加到epoll管理中。
  2. 进行上下文环境切换,由协程上下文yield到调度器的上下文。
  3. 调度器获取下一个协程上下文。Resume新的协程

IO 异步 *** 作的上下文切换的时序图如下:

回调协程的子过程

在create协程后,何时回调子过程?何种方式回调子过程?
首先来回顾一下x86_64寄存器的相关知识。汇编与寄存器相关知识还会在《协程的实现之切换》继续深入探讨的。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 用作数据存储,就是使用前要先保存原值

以NtyCo的实现为例,来分析这个过程。CPU有一个非常重要的寄存器叫做EIP,用来存储CPU运行下一条指令的地址。我们可以把回调函数的地址存储到EIP中,将相应的参数存储到相应的参数寄存器中。实现子过程调用的逻辑代码如下:

void _exec(nty_coroutine *co) {
    co->func(co->arg); //子过程的回调函数
}

void nty_coroutine_init(nty_coroutine *co) {
    //ctx 就是协程的上下文
    co->ctx.edi = (void*)co; //设置参数
    co->ctx.eip = (void*)_exec; //设置回调函数入口
    //当实现上下文切换的时候,就会执行入口函数_exec , _exec 调用子过程func
}

实现协程框架,底层原理与性能分析(C语言) 1.协程起源

协程起源 — 存在的原因?
如何使用?与线程使用有何区别?
内部是如何工作的?
原语 *** 作有哪些?分别如何实现?

2.协程实现之切换 — 上下文如何切换?代码如何实现?

运行体如何定义?调度器如何定义?
协程如何被调度?
协程多核模式 — 多核实现
协程性能测试 — 实战性能测试

不懂协程?不会用协程?

一节课让你底层原理实现协程框架

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5672130.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存