zoukankan      html  css  js  c++  java
  • 事件驱动模型

    事件驱动

    事件驱动初了解:

    通常,我们写服务器处理模型的程序时,有以下几种模型:
      (1)每收到一个请求,创建一个新的进程,来处理该请求;
      (2)每收到一个请求,创建一个新的线程,来处理该请求;
      (3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
    上面的几种方式,各有千秋,
      第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
      第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
      第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂,而这种方式就是事件驱动方式
    综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

    事件驱动模型是什么?

    目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
      1. 有一个事件(消息)队列;
      2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
      3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
      4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
      事件驱动模型一般是由事件收集器、事件发送器和事件处理器三部分组成基本单元组成

    一、select库
      select库是各个版本的linux和windows平台都支持的基本事件驱动模型库,并且在接口的定义上也基本相同,只是部分参数的含义略有差异。
      使用select库的一般步骤:创建所关注事件的描述集合。对于一个描述符,可以关注其上面的读事件、写事件以及异常发生事件,所以要创建三类事件描述符集合,分别用来收集读事件的描述符、写事件的描述符和异常事件的描述符。
      其次,调用底层提供的select()函数,等待事件的发生。select的阻塞与是否设置非阻塞的IO是没有关系的。
      然后,轮询所有事件描述符集合中的每一个事件描述符,检查是否有响应的时间发生,如果有,则进行处理。
    nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。
    可以使用--with-select_module和--without-select_module两个参数,强制nginx是否编译该库。
    二、poll库
      poll库,作为linux平台上的基本事件驱动模型,Windows平台不支持poll库。
      使用poll库的一般过程是:与select的基本工作方式是相同的,都是先创建一个关注事件的描述符集合,再去等待这些事件的发生,然后在轮询描述符集合,检查有没有事件发生,如果有,就进行处理。
      与select的主要区别是select需要为读事件、写事件、异常事件分别创建一个描述符的集合,因此在轮询的时候,需要分别轮询这三个集合。而poll库只需创建一个集合,在每个描述符对应的结构上分别设置读事件,写事件和异常事件,最后轮询的时候可以同时检查这三种事件是否发生。是select库优化的实现。
        nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。可以使用--with-poll_module和--without-poll_module两个参数,强制nginx是否编译该库。
    
    三、epoll库
        epoll库是Nginx服务器支持的高性能事件之一,它是公认的非常优秀的时间驱动模型,和poll和select有很大的不同,属于poll库的一个变种,他们的处理方式都是创建一个待处理事件列表,然后把这个事件列表发送给内核,返回的时候,再去轮询检查这个列表,以判断事件是否发生。如果这样的描述符在比较多的应用中,效率就显得低下了,epoll是描述符列表的管理交给内核负责,一旦某种事件发生,内核会把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表,epoll库得到事件列表,就开始进行事件处理了。

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

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

     

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

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

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

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

    1. 程序中有许多任务,而且…
    2. 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
    3. 在等待事件到来时,某些任务会阻塞。

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

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

    事件驱动的使用

    简而言之,事件驱动分为二个部分:第一,注册事件;第二,触发事件。

    自定义事件驱动框架,命名为:“弑君者”:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    # event_drive.py
    
    event_list = []
    
    
    def run():
        for event in event_list:
            obj = event()
            obj.execute()
    
    
    class BaseHandler(object):
        """
        用户必须继承该类,从而规范所有类的方法(类似于接口的功能)
        """
        def execute(self):
            raise Exception('you must overwrite execute')
    最牛逼的事件驱动框架

    程序员使用“弑君者框架”:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    from source import event_drive
    
    
    class MyHandler(event_drive.BaseHandler):
    
        def execute(self):
            print 'event-drive execute MyHandler'
    
    
    event_drive.event_list.append(MyHandler)
    event_drive.run()
    View Code

    如上述代码,事件驱动只不过是框架规定了执行顺序,程序员在使用框架时,可以向原执行顺序中注册“事件”,从而在框架执行时可以出发已注册的“事件”。

    from twisted.internet import protocol
    from twisted.internet import reactor
     
    class Echo(protocol.Protocol):
        def dataReceived(self, data):
            self.transport.write(data)
     
    def main():
        factory = protocol.ServerFactory()
        factory.protocol = Echo
     
        reactor.listenTCP(8000,factory)
        reactor.run()
     
    if __name__ == '__main__':
        main()
    基于事件驱动socket

    程序执行流程:

    • 运行服务端程序
    • 创建Protocol的派生类Echo
    • 创建ServerFactory对象,并将Echo类封装到其protocol字段中
    • 执行reactor的 listenTCP 方法,内部使用 tcp.Port 创建socket server对象,并将该对象添加到了 reactor的set类型的字段 _read 中
    • 执行reactor的 run 方法,内部执行 while 循环,并通过 select 来监视 _read 中文件描述符是否有变化,循环中...
    • 客户端请求到达
    • 执行reactor的 _doReadOrWrite 方法,其内部通过反射调用 tcp.Port 类的 doRead 方法,内部 accept 客户端连接并创建Server对象实例(用于封装客户端socket信息)和 创建 Echo 对象实例(用于处理请求) ,然后调用 Echo 对象实例的 makeConnection 方法,创建连接。
    • 执行 tcp.Server 类的 doRead 方法,读取数据,
    • 执行 tcp.Server 类的 _dataReceived 方法,如果读取数据内容为空(关闭链接),否则,出发 Echo 的 dataReceived 方法
    • 执行 Echo 的 dataReceived 方法

    从源码可以看出,上述实例本质上使用了事件驱动的方法 和 IO多路复用的机制来进行Socket的处理。

     
    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    from twisted.internet import reactor, protocol
    from twisted.web.client import getPage
    from twisted.internet import reactor
    import time
    
    class Echo(protocol.Protocol):
    
        def dataReceived(self, data):
            deferred1 = getPage('http://cnblogs.com')
            deferred1.addCallback(self.printContents)
    
            deferred2 = getPage('http://baidu.com')
            deferred2.addCallback(self.printContents)
    
            for i in range(2):
                time.sleep(1)
                print 'execute ',i
    
    
        def execute(self,data):
            self.transport.write(data)
    
        def printContents(self,content):
            print len(content),content[0:100],time.time()
    
    def main():
    
        factory = protocol.ServerFactory()
        factory.protocol = Echo
    
        reactor.listenTCP(8000,factory)
        reactor.run()
    
    if __name__ == '__main__':
        main()
    异步IO操作



    参考:http://www.cnblogs.com/alex3714/articles/5248247.html
    http://www.cnblogs.com/wupeiqi/articles/5040823.html


  • 相关阅读:
    常用类练习题(用字符串常用类判断输入的一段话是否为回文)
    面向对象综合练习题(动物乐园)
    多态练习题(通过UML建模语言来实现饲养员喂养动物)
    UML建模语言使用的概述
    多态练习题(员工使用不同的交通工具回家)
    多态练习题(宠物医院治疗小动物的问题 ,多态的应用:向上类型传递)
    接口练习题(书信接口)
    接口练习题(实现接口功能拓展的两种方法)
    Oracle rman 各种恢复
    Oracle rman 全备份的一个小例子
  • 原文地址:https://www.cnblogs.com/freely/p/6519838.html
Copyright © 2011-2022 走看看