java程序员学golang-——【协程】

协程

1.1 为什么需要协程

要知道为什么需要协程首先需要复习一下linux的三种IO多路复用API

1.1.1 select()

int select(int nfds,fd_set *readfds,fd_set *writefds, fd_set *exceptfds,struct timeval *timeout)
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <stropts.h>
#include <sys/poll.h>
#include <sys/stropts.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>

int main() {
    rf_set rfds;   /* 先申明一个 fd_set 集合来保存我们要检测的 socket句柄 rf_set 最大支持 1024 */
    struct timeval tv;  /* 申明一个时间变量来保存时间 */
    int retval; /* 保存返回值 */

    FD_ZERO(&rfds);   /* 用select函数之前先把集合清零 */
    FD_SET(0,&rfds); /* 把要检测的句柄socket加入到集合里 */

    tv.tv_sec=5;
    tv.tv_usec=0; /* 设置select等待的最大时间*/
    for(;;){
        retval=select(1,&rfds,NULL,NULL,&tv);  /* 每次调用都需要注册所有 rfds集合 会有用户态到内核态的切换 */
        if (retval==-1)
            perror("select()");
        else if (retval) //这里遍历的结果只知道注册 rf_set中有socket就绪了,但是并不知道哪个socket就绪了,所以需要有就绪的时候就遍历所有的socket集合
            //TODO 循环遍历 rfds中侦听的socket 判断哪个socket 就绪了
            printf("rfds have already over! ");
        else
            printf("No data within five seconds");
    }
}

看这个代码可以知道上面的select()函数有三种缺陷

1、fd的集合最大只支持1024
2、每次select都需要注册所有的rfds,会牵扯到用户态到内核态拷贝rfds的数据
3、select返回就绪状态,但是并没有返回具体哪条连接就绪了,所以需要遍历连接获取哪个socket就绪了

1.1.2 poll()

int poll(struct pollfd *fds , nfds_t nfds , int timeout);

struct pollfd{
	int fd;         //文件描述符
	short events;	//等待的事件
	short revents;	//实际发生的事件
};

poll函数使用了poolfd 这个结构体 来侦听事件,所以不存在只能侦听1024个socket的限制,但是返回值依旧是int,需要遍历结果获取socket
1、fd的集合最大支持1024
2、每次select都需要注册所有fd集合,会牵扯到用户态到内核态的fd集合拷贝
3、select返回就绪状态,但是并没有返回具体哪条就绪了,所以需要遍历fd集合获取就绪的socket连接

1.1.3 epoll()

int main(){
    epfd=epoll_creat(EPOLL_CLOEXEC); /* 创建epoll */
    
    sfd=socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0); /* 创建一个fd句柄,最后一个参数0代表非阻塞,即在epoll_wait()函数使用的时候立即返回不等待 */
    
    struct epoll_event evt={0};     /* 初始化一个事件 */
    evt.events=EPOLLIN;             /* 侦听的事件 */
    evt.data.fd=sfd;                /* 要注册的句柄 */
    
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&evt)==-1){ /* 添加fd(socket)到epoll */
        perror("epoll_ctl add epfd fail");
        exit(EXIT_FAILURE);
    }
    
    for(;;){
        nfds=epoll_wait(epfd,events,MAX_EVENTS,-1); /* 等待就绪 返回就绪的连接 */
        
        for(n=0;n<nfds;n++){
            if(events[n].data.fd==sfd){
                printf("already connect");
            }else{
                if(events[n].events & EPOLLIN){
                    printf("already read")
                }
                if(events[n].events & EPOLLOUT){
                    printf("alread write")
                }
            }
        }
    }
}

epoll()函数 分为三个步骤来进行

1、创建epoll
2、添加/删除fd到epoll
3、返回就绪的fd

所以之前select()函数带来的三个问题就被全部解决了

1、fd的集合最大支持1024
使用epoll_event结构体不存在阻塞问题

2、每次select都需要注册所有fd集合,会牵扯到用户态到内核态的fd集合拷贝
每次注册新的socket调用增量注册函数[epoll_ctl()]不需要将所有的socket集合冲洗注册到epoll

3、select返回就绪状态,但是并没有返回具体哪条就绪了,所以需要遍历fd集合获取就绪的socket连接
返回的是已经就绪的socket集合,不需要遍历所有的socket判断是否就绪

1.2 协程引入

在epoll()函数被各种语言广泛的使用下,引入了一个新的问题仔细观察epoll使用代码

    for(;;){
        nfds=epoll_wait(epfd,events,MAX_EVENTS,-1); /* 等待就绪 返回就绪的连接 */
        
        for(n=0;n<nfds;n++){
            if(events[n].data.fd==sfd){
                printf("already connect");
            }else{
                if(events[n].events & EPOLLIN){
                    printf("already read")    /* 可读以后要唤醒业务线程 */
                }
                if(events[n].events & EPOLLOUT){
                    printf("alread write")
                }
            }
        }
    }

1、epoll_wait()函数如果没有侦听到事件,在大多数场景下(依据socket函数声明时候的最后一个入参如果是0则直接返回)是直接返回的。
2、这样做会有一个空转的for循环,一直在询问epoll_wait()是否有就绪的socket。
3、在java等无协程语言中就是一个线程池在不断的轮训自己注册的socket是否就绪,就绪后就会唤醒另一条线程去处理相应的业务逻辑,带来了大量的线程上下文切换
此时使用线程就显得额外笨重。

1.2.1 协程的引入

那么有没有一种线程的创建不需要用户态到内核态的转换,又可以具备线程的特性呢?
答案是有:协程或者说叫用户态线程

协程的实现

首先我们先来看看线程是怎么实现的,线程的组成部分有这么几块
java 伪代码

public class Thread{
    private Queue stack;            //一个代码栈
    private int pc;                 //指向要执行的下一行代码的指针
    private ThraadInfo info;        //线程信息
}

class ThreadInfo {
    private int id;                 //线程ID
    private String name;            //线程名称
    private level;                  //线程的优先级(获取cpu时间片的优先级)
    private state;                  //状态
}

可以看到线程实际上是个数据结构,每次中断的时候由于记录了栈帧所以下次恢复的时候依旧知道程序执行到哪里了。聪明的人已经开始想了,既然线程是个数据结构为啥我不能自己写个线程?
cpu把时间片分给内核线程,内核线程轮询我自己写的线程,不就相当于把时间片分给我自己写的线程了。这个自己写的线程就是我们俗称的协程。既然协程没有调用操作系统的api去创建,那么这个协程也就理所应当的是处于用户态,所以也叫用户态线程。

1.2.2 协程引入后的epoll

    for(;;){
        nfds=epoll_wait(epfd,events,MAX_EVENTS,-1); /* 等待就绪 返回就绪的连接 */
        
        for(n=0;n<nfds;n++){
            if(events[n].data.fd==sfd){
                printf("already connect");
            }else{
                if(events[n].events & EPOLLIN){
                    printf("already read")         /* 如果可读了这个时候找到对应的协程 恢复协程上下文,不牵扯到线程唤醒 继续执行协程 */
                }
                if(events[n].events & EPOLLOUT){
                    printf("alread write")
                }
            }
        }
    }

1.3 协程的优缺点

先说缺点
1、协程是个用户态的线程,由线程模拟实现,所以本身会带来一定的内存开销
2、在程序为计算密集型的情况下(非io密集型)由于带来了额外的协程上线文切换的开销,性能不一定有线程使用强
3、在阻塞io的情况下或者说不使用epoll进行io处理,对大量的协程阻塞对性能并没有任何好处反而会带来大量的内存开销
4、上文中说了协程是用户态的实现,目前各个语言对协程的支持有好有坏,不过目前的支持来看都是非抢占式的方式来调度协程的,即线程调用协程除非协程执行完或者协程碰到阻塞主动让出cpu进入等待队列,否则协程不会被线程中断。故协程具有非抢占式调度的所有缺点 例如:同一时刻一条线程只能执行一个协程,并且只能由协程主动交出控制权,所以对于长时间的密集计算型程序,协程的并发能力不如抢占式调度的线程友好

优点
1、协程的上下文切换不要从用户态进入到内核态
2、协程的创建使用用户态空间,所以理论上可以做到有多大内存就有多少协程,并且由于创建和切换都在用户态执行,所以协程的上下文切换开销对比线程几乎可以忽略不计。也就是说可以不使用像线程一样的池化技术对于编码来说非常友善
3、协程是非抢占式的调度,具有非抢占式调度的所有优点 例如:对于经常需要主动出让时间片的程序非常友好

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值