IO模型——事件驱动思想理论

 Pala   2017-07-05 22:10   276 人阅读  0 条评论

IO多路复用属于IO模型之一,IO多路复用应用的思想——事件驱动思想

编程范式:

    面向过程编程、函数式编程(map,reduce,filter)、面向对象编程

事件驱动编程思想:

    一种编程范式(写代码的一种思路)

遇到IO操作怎么办?

        解决方法:

        很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。

        这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

        对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题

传统的编程是如下线性模式的:

        开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束

        每一个代码块里是完成各种各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,唯一能够改变这个流程的是数据。输入不同的数据,根据条件语句判断,流程或许就改为A--->C--->E...--->结束。每一次程序运行顺序或许都不同,但它的控制流程是由输入数据和你编写的程序决定的。如果你知道这个程序当前的运行状态(包括输入数据和程序本身),那你就知道接下来甚至一直到结束它的运行流程。

 对于事件驱动型程序模型,它的流程大致如下:

        开始--->初始化--->等待

        与上面传统编程模式不同,事件驱动程序在启动之后,就在那等待,等待什么呢?等待被事件触发。传统编程下也有“等待”的时候,比如在代码块D中,你定义了一个input(),需要用户输入数据。但这与下面的等待不同,传统编程的“等待”,比如input(),你作为程序编写者是知道或者强制用户输入某个东西的,或许是数字,或许是文件名称,如果用户输入错误,你还需要提醒他,并请他重新输入。事件驱动程序的等待则是完全不知道,也不强制用户输入或者干什么。只要某一事件发生,那程序就会做出相应的“反应”。这些事件包括:输入信息、鼠标、敲击键盘上某个键还有系统内部定时器触发。

一、事件驱动模型介绍

1.1 什么是事件驱动模型:

        英语:Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键键盘的按键动作)或者是由其他程序的消息来决定的。相对于批处理程序设计(batch programming)而言,程序运行的流程是由程序员来决定。批量的程序设计在初级程序设计教学课程上是一种方式。然而,事件驱动程序设计这种设计模型是在交互程序(Interactive program)的情况下孕育而生的。

        事件驱动程序可以由任何编程语言来实现,然而使用某些语言来撰写会比其他的语言来的简单。有些集成开发环境(简称IDE)也会影响实现事件驱动程序设计的难易程度。有的 IDE 会使的开发工作变的很简单,有的则否。

1.2 事件驱动模型组成:

        事件驱动模型一般是由事件收集器事件发送器事件处理器三部分组成基本单元组成。

1.3 事件驱动模型两种思路:

轮询方式

       传统的邮件,邮递员把它放到你家的邮箱里。因为你不知道什么时候有邮件,所以你要经常去检查邮箱,最近到底有没有邮件。这就是所谓的轮询方式,你要时常去检查,有没有发生事件发生,当你检查到有事件发生时,你采取相应措施,处理相关事件。

事件驱动方式

       现代的电子邮件,你不用自己去查看邮箱,如果有新邮件,电脑会给你发消息,提示你有新邮件,然后你去查看邮箱。这就是所谓的事件驱动(消息通知),你不用去关心事件什么时候发生,当有事件发生时,会有人通知你,事件发生了,然后你再采取相应的措施,处理相关事件。

轮询和事件驱动方式利弊

    轮询弊端:

           需要做很多没有意义的检查,造成CPU资源浪费。换句话说CPU利用率不高

    事件驱动利弊:

           优点:解决轮询的弊端

           弊端:模型复杂,程序写起来复杂,

 应用:

        高吞吐、大并发的网络服务器,都会采用事件驱动模型,其优点就是CPU利用率高,写出来的程序效率比较高。

1.4 常见事件驱动(消息通知)模型:



上图中,EventMonitor是用来从操作系统获取事件的线程,EventProcessor是实际来处理事件的线程。在EventMonitor中调用select/poll/epoll_wait获取事件(事件收集器),然后把发生的事件,以消息的方式通知(事件发送器)给EventProcessor, EventProcessor收到消息后,处理所发生的事件(事件处理器)。这里EventProcessor可以有多个,同一个fd的上事件,一般在同一个EventProcessor上处理。这样,EventMonitor的工作就是获取事件,是比较轻量级的,重量级的运算处理工作都由EventProcessor来处理,通过调整EventProcessor的个数,提高CPU的利用效率,从而提高整个程序的性能。

        1.5 服务器处理模型的三种模型:       

        1、每收到一个请求,创建一个新的进程,来处理该请求;

        2、每收到一个请求,创建一个新的线程(协程),来处理该请求;

        3、每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求(协程)

        上面的几种方式,各有千秋,

        第一种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

        第二种方式,由于要涉及到线程的同步,有可能会面临死锁等问题(死锁和递归锁)。

        第三种方式,在写应用程序代码时,逻辑比前面两种都复杂。

        结论:第三种就是协程、事件驱动的方式,一般普遍认为第(3)种方式是大多数网络服务器采用的方式 

二、事件模型代码示例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<p onclick="fun()">点我呀</p>

<script type="text/javascript">
    function fun() {
          alert('约吗?')
    }
</script>
</body>

</html>

这个是一个html页面,只要点击“点我呀”,就会触发一个事件弹出“约吗”对话框。

在UI编程(图形界面编程)中,常常要对鼠标点击进行响应,如何获取鼠标点击的两种方式:

        1、轮询方式

        1、CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?

        2、如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;            

        3、如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题; 

        2、事件驱动模型思路

        目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。

        1、有一个事件(消息)队列;

        2、鼠标按下时,往这个队列中增加一个点击事件(消息);

        3、有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;

        4、事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

        事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程

        让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

threading_models.png

        在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

        在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

        在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

        当我们面对如下的环境时,事件驱动模型通常是一个好的选择:

  1.     程序中有许多任务,而且…

  2.     任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…

  3.     在等待事件到来时,某些任务会阻塞。

        当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。(协程)

        网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

三、总结


       1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较。实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在需要等待某个条件触发时,会不断地检查这个条件,直到条件满足,这是非常浪费cpu时间的(这就是轮询)。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,当然程序也可自行决定不释放cpu),当事件触发时被操作系统唤醒,这样就能更加有效地使用cpu.

        2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照一定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。

        3.事件驱动的程序,必定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件

        4.事件驱动的程序的行为,完全受外部输入的事件控制,所以,事件驱动的系统中,存在大量这种程序,并以事件作为主要的通信方式。

        5.事件驱动的程序,还有一个最大的好处,就是可以按照一定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性往往被用于保证某些过程的原子化。

        6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机可能是非事件驱动的。

注意,事件驱动的监听事件是由操作系统调用的cpu来完成的



本文地址:http://chenxm.cc/post/124.html
版权声明:本文为原创文章,版权归 Pala 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

还没有留言,还不快点抢沙发?