在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
在比较这两个模式之前,我们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步。同步和异步是针对应用程序和内核的交互而言的。同步指的是:同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,由用户进程自己触发自己去查询。而异步是指:用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知,用户进程不需要直接进行IO操作,真正的IO操作由内核来完成。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO。我们用一个客人去餐厅找服务员点菜的例子来分别简单的解释一下这几种IO模型。
同步阻塞IO:
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。这就比如一个客人进了餐厅要向服务员点菜(发起IO请求),那么这个服务员就必须一直站在他身边等(等待IO操作)直到他看完菜单点完菜(完成IO操作)以后服务员才能去通知厨房或者去服务其他的客人。这不仅浪费资源而且效率低下。
同步非阻塞式IO:
在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的cpu资源浪费。这就好比一个客人来餐厅吃饭,服务员拿菜单给他选择(发起一个IO操作),在客人看菜单的时间服务员不会傻站着她可以去服务其他人或者干其他的事,但是服务员要不断的去询问这位客人选完了吗(定时查询IO),一旦客人回答可以了服务员就去给客人下订单(查询到IO就绪之后响应IO操作)。这种方式比阻塞式的IO要高效许多。
异步阻塞IO:
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!
异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
异步操作就好比是客人来餐厅吃饭服务员先给客人一张菜单和订单(发起一个IO操作)然后服务员去做其他事情了,一旦客人决定好了就会自己把菜名写在订单上(IO操作有内核自动完成)然后通知服务员订单已经填好了(通知应用程序IO操作已经完成了)然后服务员将订单交给厨房(用户进程处理IO数据)
搞清楚了以上概念以后,我们再回过头来看看,Reactor模式和Proactor模式。
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们分别以读操作和写操作为例来看看Reactor中的具体步骤:
读取操作:
1. 应用程序注册读就绪事件和相关联的事件处理器。
2. 事件分离器等待事件的发生
3. 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理,写入操作类似于读取操作,只不过第一步注册的是写就绪事件。
下面我们来看看Proactor模式中读取操作和写入操作的过程:
读取操作:
1.应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
2. 事件分离器等待读取操作完成事件。
3. 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
4. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备。
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
现在有许多开源的网络库无非都是用的这两种IO模式。使用Rector模式的有libevent、muduo、Sockets等。而使用Proactor模式的我接触到的不多,Boost的网络库asio用的就是Proactor模式。由于Proactor模式不怎么常用,我就用了Boost的asio来写一个服务器和客户端通信的简单例子来给自己加深理解。首先是服务器。
class CTcpServer { public: typedef boost::shared_ptr<ip::tcp::socket> SockPtr; CTcpServer(io_service& io) :ios(io),acceptor(ios,ip::tcp::endpoint(ip::tcp::v4(),6688)) { start(); } ~CTcpServer() { } private: void WriteHandler(const boost::system::error_code& e) { //这个函数是IO写操作完成后的回调函数,当内核完成了IO写操作后这个函数将会被调用 cout<<"Send Message Complete."<<endl; } void AcceptHandler(const boost::system::error_code& ec,SockPtr sock) { //这个函数是接受到客户端连接之后的回调函数,当收到客户端的连接到来之后该回调函数就会被调用 if(ec) { return; } cout<<"Client:"<<endl; cout<<sock->remote_endpoint().address()<<endl; sock->async_write_some(buffer("Hello Asio"),boost::bind(&CTcpServer::WriteHandler,this,placeholders::error));//向IO写入数据被注册IO写操作完成之后执行的回调函数 start(); } void start(void) { SockPtr pSocket(new ip::tcp::socket(ios)); acceptor.async_accept(*pSocket,boost::bind(&CTcpServer::AcceptHandler,placeholders::error,pSocket));//注册IO收到socket连接之后调用的回调函数 } private: io_service &ios; ip::tcp::acceptor acceptor; }; int main() { try { cout<<"server start!"<<endl; io_service ios; CTcpServer server(ios); ios.run(); cout<<"server end!"<<endl; } catch(std::exception& e) { cout<<e.what()<<endl; } return 0; }
从上面的代码看,代码中并没有直接的进行IO读写操作,而是注册了IO操作完成之后所要执行的回调函数,这就是Proactor模式的特征。IO操作由内核来执行,我们要做的是传入一个缓冲区,将要写入的的数据传给内核或是直接读取缓冲区中的数据。
接下来看客户端:
CTcpClient::CTcpClient(io_service &io) :ios(io),ep(ip::address::from_string("127.0.0.1"),6688) { start(); } CTcpClient::~CTcpClient() { } void CTcpClient::start() { SocketPtr pSocket(new ip::tcp::socket(ios)); pSocket->async_connect(ep,boost::bind(&CTcpClient::ConnectHandler,pSocket)); //连接到主机127.0.0.1,并且设置连接成功之后的回调函数 } void CTcpClient::ConnectHandler(const boost::system::error_code &ec,CTcpClient::SocketPtr pSock) { //连接成功之后的回调函数 if(ec) {//处理出错代码,出错就返回 return; } cout<<"recieve from:"<<pSock->remote_endpoint().address()<<endl; boost::shared_ptr<vector<char> > strings(new vector<char>(100,0)); pSock->async_read_some(buffer(*strings),boost::bind(&CTcpClient::ReadHandler,strings)); //读取缓冲区的数据,设置读取数据完成之后的回调函数,该函数传入一个数据的缓冲区,内核将读到的IO数据存入缓冲区中 } void CTcpClient::ReadHandler(const boost::system::error_code &ec,boost::shared_ptr<vector<char> > strings) { //读取数据完成之后的回调函数 if(ec) {//如果出错就返回,处理出错代码 return; } cout<<"recieve data:"<<endl; cout<<&(*strings)[0]<<endl; } int main() { try { cout<<"Client start!"<<endl; io_service ios; CTcpClient client(ios); ios.run(); } catch(const std::exception& e) { cout<<e.what()<<endl; } return 0; }
从上面的代码可以看出,读取IO操作也没有直接去操作IO,而是内核将数据存入了你给的缓冲区中。