作者: 雪山肥鱼
时间:20210430 22:14
目的:事件并发简介
# 背景
# 简介:An Event Loop
# 重要的基础API: select()
## select()举例 一
## select()举例 二
## 为什么更简单? 无锁
# 问题: 阻塞的系统调用
# 问题: 状态的管理
# 问题: event-based 其他困难
# 小结
背景
在现代的操作系统中,基于事件的并发越发流行。比如node.js.
Event-base concurrency 针对两层问题。
- 管理并发的正确性,比如死锁,活锁,等其他多线程问题
- 多线程问题中,程序员几乎没有权限控制调度。程序员只是简单的创建了线程,依赖OS去合理的调度。
要实现一个在各种不同负载下,都能够良好运行的通用调度程序,是极有难度的。因此,某些时候操作系统的调度并不是最优的。
不用多线程,去构建并发服务器,同时保证对并发的控制,且避免之前多线程面临的问题,是个问题的关键
简介:An Event loop
- 等待事件的发生
- 检查事件类型
- 做事情:I/O request or 调度其他事件
//even loop
while(1) {
events = getEvents();
for( e in events)
processEvent(e);
}
当事件正在处理时,他是系统中唯一发生的事件。所以调度器要做的就是 下一个事件是什么。对调度的掌控是event based contorl 的一大优点。
那么如何准确的决定下一个事件是什么,特别是当考虑到 network 和 disk 的 I/O。
特别是一个event based server 如何判断一条消息属于它的消息是否已经到达。
重要的基础API: select( ) (or poll)
select() & poll 的目的:
检查是否有任何应该关注的输入I/O。如网络应用程序(web 服务器) 希望检查是否有网络数据包已经到达,以便为他们提供服务。
int select(int nfds,
fd_set * readfds,
fd_set * writefds,
fd_set * errorfds,
struct timval * timeout);
- nfds, 检测后续参数中每一个 fds_set 中 0 到第 nfds-1 个 fdS
- slecect 检测 readfds(准备好读), writefd(准备好写), errorfds(准备好接受意外事件), timeout(超时)。
注意点:
- select 可以让你同时检查 是否有可读的文件描述符,是否有可写的文件描述符。
- 可读: 有包进来,读包, 处理包
- 可写: 写入数据把包发出去
2.超时设置成NULL,代表 select 可以等下去,直到某个fd准备就绪(不太理解这个准备就绪,是有数据要读可写?),当然也可以设置超时事件。
select 和 poll 的原理大致相同。
当然还有epoll 我们后续讨论
这些基于事件的api构造了 非阻塞的 事件循环。可以用来检查 从sockets 传来的数据,并在需要时做出回复。
select ( ) 举例。
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
//open and setup a bunch of sockets (not shown)
//main loop
while(1) {
fd_set readFDs;
FD_ZERO(&readFDs);
// now set the bits for the descriptors
// this server is intrested in
// for simplicity, all of threm from imn to max)
int fd;
for (fd = minFD; fd < maxFD; fd++)
FD_SET(fd, &readFDs);
// do the select
int rc = select(maxFD +1, &readFDs, NULL, NULL, NULL);
// check wich acturally have data using FD_INSSET()
int fd;
for(fd = minFD; fd < maxFD; fd++)
if(FD_ISSET(fd, &readFDs))
processFD(fd);
}
}
假设开了 很多socktes, 我们用select 查看哪些网络描述符在他们上面有传入消息。
- 清空fd_set
- 设置 fd_set,我们监视从 0 到 maxFD-1 这些fd
- select 检测
- 一旦select 返回,重新过一遍fd_set 的每一个 fd。得到数据并处理。
真正的服务器要比者复杂的多。涉及在发送消息,发出磁盘I/O和许多其他细节。 后续继续学习 UNIX 环境高级编程把。
为什么更简单? 无锁
在单cpu 和 基于事件的应用中,不存在并行问题。特别是因为 event-based 当处理一个时间时,不会被其他线程中断,event-based是单线程的(单CPU)。
提示,不要阻塞based event server.
问题:阻塞的系统调用
当事件发起的请求中,有阻塞的系统调用该怎么处理呢?
例如 客户端发向服务器发起请求,求取读服务器磁盘,并返回文件内容。就像http请求一样。
通常event handler 会发起 open() 系统调用 打开文件,紧跟着一些列的read() 去 读文件。当文件读入内存,服务器会发送结果给客户端。
open 和 read 函数向内存发送请求时,数据可能还不在内存中,那么就要访问硬盘,则会花费很多时间才能给client返回数据。
对于 thread-based server 来说,这不是事儿,当线程发起I/O请求时,CPU 会切换到其他线程,可以是服务器正常运行。这种I/O 复用让多线程服务看起来 自然 直接。
对于event - based 的服务器来说,一个事件只要block 了,那服务器就会处于idle态,会造成资源的浪费。所以,对于event-based 服务器来说,非阻塞的调用时必须的。
解决方案: 异步I/O
现代操作系统中会引入 Asynchronous I/O。App 发出系统调用,然后立刻返回。
存在另外的接口,让App 能够确定确定I/O 是否已经完成。
int aio_read();
int aio_error();
比如说 异步方式去读,然后立刻返回,定期利用 aio_error()检查 异步I/O是否完成。
非常明显的缺点,总是要检查 I/O(如果时100个I/O请求呢)是否已经结束,很烦。
Unix 采用的方式 是 中断,interrupt. 当I/O 结束时,UNIX 会发送信号给调用者,caller 就不用一直去问,I/O 有没有结束了。
轮询 vs 中断 在设备系统中也经常见到。尤其时后续的I/O 设备章节。
补充 : 信号
kill -l 查看所有信号
#include <stdio.h>
#include <signal.h>
void main( int arg) {
printf("stop wakin me up...\n");
}
int main(int argc, char ** argv) {
signal(SIGHUP, handler);
while(1)
;
return 0;
}
关于 sighup 信号的介绍,进程 进程组 会话之间的关系
https://blog.csdn.net/z_ryan/article/details/80952498
通常是 事件+多线程 hybird approach
- 事件用于处理网络包
- 多线程/线程池 用来管理未完成的I/O
另一个问题: 状态的管理
- 对于多线程,所有的状态都在线程独占的 stack 中
- 对于事件,发出异步I/O请求后,需要打包当前的程序状态,等着I/O请求完毕,再回头利用这些状态,进行后续处理。
举例多线程:
int rc = read(fd, buffer, size);
rc = write(sd, buffer,size);
read读好后,线程立刻直到去往哪一个fd(sd)里写。
对于异步的事件处理机制来说,就比较麻烦,需要定期检查 aio_error()的值,或者等着被通知 fd已经准备就绪。那么后续event-based server 收到 IO 结束的通知,该怎么做呢,应该如何找到要发送的 sd??
- 在某个地方记录 完成处理该事件的 所需要的信息(存接下来要用到的SD)
- I/O 完成时,找到所需信息(sd),处理事件
以上述代码为例。
- 将sd 存于某处,并且可以通过fd 查询到,比如一个 hash table
- 当 I/O 结束,event handler 会通过fd 查找sd。
- 往sd 里写东西,完成剩余工作
event-based 的其他困难
- event-based 搬运到 多CPU 上,就会遇到很多困难。event-based单线程的 eventhandler 优势,荡然无存。在当代多核cpu的前提背景下,无所的 事件 处理机制,是永远不会存在的了。
- 面对系统类的事件,比如 paging,在等page - fault 的时候,event 的阻塞。这类隐式的 block 难以避免,可能会造成较大的性能问题
- 随着事件的退役,基于事件的服务器代码难以维护,一旦一个routine从同步进化成异步,那么之前所涉及的代码都要修改,也必须要服从新的特性。阻塞事件对于服务器的性能是才难的,因此程序员必须时钟注意每个事件API 不同参数下的不同应对。
- 异步磁盘I/O 和 异步 网络I/O 还没把发进行统一和整合,这是狠难的。比如 select 管理 未完成的I/O ,这涉及 select for networking 和 select for dis I/O 的 组合
小结
- Event-based 相较于 multi-thread 来说,提供了更好的调度掌控
- 但代价是面对 数据的传递 以及 系统级(pagiging),处理起来比较繁琐,甚至很有难度。
- 没有哪一种是完美的。
- PDZ99 尝试阅读