线程(七): 基于事件的并发

作者: 雪山肥鱼
时间:20210430 22:14
目的:事件并发简介

# 背景
# 简介:An Event Loop
# 重要的基础API: select()
  ## select()举例 一
  ## select()举例 二
  ## 为什么更简单? 无锁
# 问题: 阻塞的系统调用
# 问题: 状态的管理
# 问题: event-based 其他困难
# 小结

背景

在现代的操作系统中,基于事件的并发越发流行。比如node.js.
Event-base concurrency 针对两层问题。

  1. 管理并发的正确性,比如死锁,活锁,等其他多线程问题
  2. 多线程问题中,程序员几乎没有权限控制调度。程序员只是简单的创建了线程,依赖OS去合理的调度。
    要实现一个在各种不同负载下,都能够良好运行的通用调度程序,是极有难度的。因此,某些时候操作系统的调度并不是最优的。
    不用多线程,去构建并发服务器,同时保证对并发的控制,且避免之前多线程面临的问题,是个问题的关键

简介:An Event loop

  1. 等待事件的发生
  2. 检查事件类型
  3. 做事情: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(超时)。

注意点:

  1. select 可以让你同时检查 是否有可读的文件描述符,是否有可写的文件描述符。
  • 可读: 有包进来,读包, 处理包
  • 可写: 写入数据把包发出去

2.超时设置成NULL,代表 select 可以等下去,直到某个fd准备就绪(不太理解这个准备就绪,是有数据要读可写?),当然也可以设置超时事件。
select 和 poll 的原理大致相同。
当然还有epoll 我们后续讨论
这些基于事件的api构造了 非阻塞的 事件循环。可以用来检查 从sockets 传来的数据,并在需要时做出回复。

select ( ) 举例。

#include 
#include 
#include 
#include 
#include 

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 查看哪些网络描述符在他们上面有传入消息。

  1. 清空fd_set
  2. 设置 fd_set,我们监视从 0 到 maxFD-1 这些fd
  3. select 检测
  4. 一旦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 查看所有信号


信号种类.png
#include 
#include 

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??

  1. 在某个地方记录 完成处理该事件的 所需要的信息(存接下来要用到的SD)
  2. I/O 完成时,找到所需信息(sd),处理事件

以上述代码为例。

  1. 将sd 存于某处,并且可以通过fd 查询到,比如一个 hash table
  2. 当 I/O 结束,event handler 会通过fd 查找sd。
  3. 往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 的 组合

小结

  1. Event-based 相较于 multi-thread 来说,提供了更好的调度掌控
  2. 但代价是面对 数据的传递 以及 系统级(pagiging),处理起来比较繁琐,甚至很有难度。
  3. 没有哪一种是完美的。
  4. PDZ99 尝试阅读

你可能感兴趣的:(线程(七): 基于事件的并发)