服务器的目的是让程序同时执行多个任务。
服务器并发场景是在程序IO密集型有优势。因为IO操作速度远没有CPU的计算速度快。程序阻塞IO将浪费大量CPU时间。程序计算密集型的,并发编程反而没有优势。
一种情况accept和recv在同一个线程(nginx也是如此,但是nginx的event模型,这个例子当发送io阻塞没有使用event模型设计)
---------- -------- -------- -------------------------------- ------------------
| accept |-->| recv |-[slow]->| send | --> | recv peer's close fin packet | -->| inacitve close |
---------- -------- -------- -------------------------------- ------------------
拆散IO前的代码逻辑和 拆散IO后的代码逻辑。拆散后的IO通知需要select/epoll非阻塞多IO模型进行IO完成事件通知。假设还没有select/epoll的年代。如果是IO前和IO后都还是同一个线程处理,那么这里accept和read会占用同一片cpu时间内,需要注意到避免read过久而导致不能及时accept响应客户端请求。这时候,有像一些更多线程的模型。比如说Reactor和Proactor模型。Reactor的read部分(IO后)放在另一个线程进行。Proactor的accept和recv不放在同一个线程。 Reactor和Proactor模型。主线程只负责监听文件描述符是否有事件发生,有的话唤起工作进程,其他读写数据,接受新的连接,处理都在工作进程进行。
Proactor模型:将所有IO操作都交给主线程和内核来处理。工作线程只负责业务逻辑。
-------------------------------
----| event handler (other thread)|
|MQ| -------------------------------
---------- ------------------- ------------------
| accept | --> | add epoll event | --> | NON-Block recv |
---------- ------------------- ------------------
这里的改变有两点,一点是需要做代码的拆分,引入event handler,代码变得分离破碎了些。引入了MQ,多线程数据通过请求队列进行同步,但是多线程共享同一个地址空间,所以需要解决竞争问题。
如果又要实现IO异步通知机制,解放cpu,又要写成同步代码逻辑,那我们试试用协程序的方式写下。
-------- ------------------ ----------- ----------- --------------- ------------ ----------------
|accept|-->|read_async begin|-->|epoll add|-->|cpu yield|-->|event handler|-->|cpu resume|-->|read_async end|
-------- ------------------ ----------- ----------- --------------- ------------ ----------------
这里的代码改造关键点有自定义的read_async函数或者hook系统的标准调用read。逻辑替换成读操作会转化成一个epoll read event的add操作。add完出让自己的cpu控制权,此时cpu控制权又回到主协程(控制权的交还是保存并跳出当前的栈空间,但不是return操作,因为此时后续还有等read到数据的后续操作。这里会有设置一个调用栈的中断点。只有等有数据的时候恢复此调用栈到中断点位置。中断点的栈恢复是在event handler执行,又可以处理剩下的代码逻辑)。此时主协程从上一个操作出来可以accept别的请求。
原本切换线程的动作使用协程
struct async_job_st {
async_fibre fibrectx;
int (*func) (void *);//协程的IO程序逻辑函数,该函数可能会有IO逻辑
void *funcargs;//相应的函数参数
int ret;
int status;
ASYNC_WAIT_CTX *waitctx;
};
typedef struct async_fibre_st {
ucontext_t fibre;//用来保存当前协程所在的栈空间,恢复该栈可以恢复该协程的运行
jmp_buf env;//这个可能不是必须的?因为已经有了ucontext接口,不需要_setjmp/_longjmp
int env_init;//这个不知道干嘛,感觉是为了切换ucontext和jmp两套接口
} async_fibre;
ucontext_t的创建办法(差不多都是这个固定写法)
if (getcontext(&fibre->fibre) == 0) {//获取到一个ucontext_t对象,后续初始化该context
fibre->fibre.uc_stack.ss_sp = OPENSSL_malloc(STACKSIZE);ucontext_t保存一个栈空间
if (fibre->fibre.uc_stack.ss_sp != NULL) {
fibre->fibre.uc_stack.ss_size = STACKSIZE;//此处为32KB,比栈的16M少很多
fibre->fibre.uc_link = NULL;//设置下一个ucontext_t, 相当于一串context挨个执行过去,如果为空,则返回
makecontext(&fibre->fibre, async_start_func, 0);//指定这个栈响应的 函数指针,即跳转到该context需要调用的PC入口
return 1;
}
}
协程栈的生成包括寄存器,信号掩码,以及栈。
数量动态可调节的池子。
struct async_pool_st {
STACK_OF(ASYNC_JOB) *jobs;//池子,如果栈不为空,则get从栈中pop出可用job,release时push回stack中。如果栈为空,则new。
size_t curr_size;//初始化池子大小。
size_t max_size;//job数最大值
};
async_fibre_swapcontext(old, new)
取出old->env,保存,调到新的new->env
async_fibre_swapcontext(new, old)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。