进入正题,简要说一下asio的实现原理吧。在win32平台上,asio是基于IOCP技术实现的,我以前也用过IOCP,却没想到居然能扩展成这样,真是神奇!在其他平台下还会有别的方法去实现,具体见io_service类下面这部分的源码:
这部分代码其实就在boost::asio::io_service类声明中的最前面几行,可以看见在不同平台下,io_service类的实现将会不同。很显然,windows平台下当然是win_iocp_io_service类为实现了(不过我一开始还以为win_iocp_io_service是直接拿出来用的呢,还在疑惑这样怎么有移植性呢?官方文档也对该类只字不提,其实我卡壳就是卡在这里了,差点就直接用这个类了^_^!)。
那么就分析一下win_iocp_io_service的代码吧,这里完全是用IOCP来路由各种任务,大家使用post来委托任务,内部调用的其实是IOCP的PostQueuedCompletionStatus函数,然后线程们用run来接受任务,内部其实是阻塞在IOCP的GetQueuedCompletionStatus函数上,一旦有了任务就立即返回,执行完后再一个循环,继续阻塞在这里等待下一个任务的到来,这种设计思想堪称神奇,对线程、服务以及任务完全解耦,灵活度达到了如此高度,不愧为boost库的东西!我只能有拜的份了...
说一下总体的设计思想,其实io_service就像是劳工中介所,而一个线程就是一个劳工,而调用post的模块相当于富人们,他们去中介所委托任务,而劳工们就听候中介所的调遣去执行这些任务,任务的内容就写在富人们给你的handler上,也就是函数指针,指针指向具体实现就是任务的实质内容。其实在整个过程中,富人们都不知道是哪个劳工帮他们做的工作,只知道是中介所负责完成这些就可以了。这使得逻辑上的耦合降到了最低。不过这样的比喻也有个不恰当的地方,如果硬要这样比喻的话,我只能说:其实劳工里面也有很多富人的^o^! 。很多劳工在完成任务的过程中自己也托给中介所一些任务,然后这些任务很可能还是自己去完成。这也难怪,运行代码的总是这些线程,那么调用post的肯定也会有这些线程了,不过不管怎么说,如此循环往复可以解决问题就行,比喻不见得就得恰当,任何事物之间都不可能完全相同,只要能阐述思想就行。
最后还要说明的一点就是:委托的任务其实可以设定执行的时间的,很不错的设定,内部实现则是通过定时器原理,GetQueuedCompletionStatus有一个等待时间的参数似乎被用在这方面,还有源码中的定时器线程我并没有过多的去理解,总之大体原理已基本掌握,剩下的就是使劲的用它了!!!
另外为了方便人交流,在这里插入一些代码可能更容易让人理解吧,
下面这个是启动服务时的代码:
//启动服务
typedef boost::thread_group CThreadPool;
void CServer::Run(){
CThreadPool oThreadPool;
for(int i = 0; i < iThreads; i++){//
oThreadPool.create_thread(boost::bind(&boost::asio::io_service::run, &oIoSrv));
}
//等待所有工作线程结束运行
oThreadPool.join_all();
}
在打开前就得分配好任务,否则线程们运行起来就退出了,阻塞不住,任务的分配就交给open函数了,它是分配了监听端口的任务,一旦有了连接就会抛出一个任务,其中一个线程就会开始行动啦。
typedef boost::asio::ip::tcp::resolver CResolver;
typedef boost::asio::ip::tcp::resolver::query CQuery;
typedef boost::asio::ip::tcp::endpoint CEndPt;
void CServer::open(const String& address, const String& port, uint32 nWorkers /*= DEFAULT_WORKER_COUNT*/)
{
CResolver oResolver(oIoSrv);
CQuery oQuery(address, port);
CEndPt oEndPt = *oResolver.resolve(oQuery);
oAcceptor.open(oEndPt.protocol());
oAcceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
oAcceptor.bind(oEndPt);
oAcceptor.listen();
pNextConn = new CConnection(this);
oAcceptor.async_accept(pNextConn->GetSock(), boost::bind(&CServer::OnConn, this, boost::asio::placeholders::error));
iThraeds = nWorkers > 4 ? 4, nWorkers;
}
open函数中给io_service的一个任务就是在有链接访问服务器端口的情况下执行ServerFramework::__onConnect函数,有一点需要格外注意的,io_service必须时刻都有任务存在,否则线程io_service::run函数将返回,于是线程都会结束并销毁,程序将退出,所以,你必须保证无论何时都有任务存在,这样线程们即使空闲了也还是会继续等待,不会销毁。所以,我在ServerFramework::__onConnect函数中又一次给了io_service相同的任务,即:继续监听端口,有链接了还是调用ServerFramework::__onConnect函数。如果你在ServerFramework::__onConnect执行完了还没有给io_service任务的话,那么一切都晚了...... 代码如下:
void CServer::OnConn(const BoostSysErr& e)
{
if(e){
cout << e.message().c_str() << endl;
return;
}
CConnection* pConn = pNextConn;
pNextConn = new CConnection(this);
// 再次进入监听状态
oAcceptor.async_accept(pConn->GetSock(), boost::bind(&CServer::OnConn, this, boost::asio::placehoulders::error));
// 处理当前链接
AddConn(pConn);
pConn->start();
}
最后,展示一下这个类的所有成员变量吧:
// 用于线程池异步处理的核心对象
boost::asio::io_service oIoSrv;
// 网络链接的接收器,用于接收请求进入的链接
boost::asio::ip::tcp::acceptor oAcceptor;
// 指向下一个将要被使用的链接对象
Connection* pNextConn;
// 存储服务器链接对象的容器
ConnectionSet oConnSet;
// 为链接对象容器准备的同步锁,防止并行调用mConnections
boost::mutex oMutex;
// 为控制台输出流准备的strand,防止并行调用std::cout
AsioService::strand oConsole;
// 工作线程的数量
uint32 iThreads;
io_servie 实现了一个任务队列,这里的任务就是void(void)的函数。Io_servie最常用的两个接口是post和run,post向任务队列中投递任务,run是执行队列中的任务,直到全部执行完毕,并且run可以被N个线程调用。Io_service是完全线程安全的队列。
提供的接口有run、run_one、poll、poll_one、stop、reset、dispatch、post,最常用的是run、post、stop
l Io_servie是接口类,为实现跨平台,采用了策略模式,所有接口均有impl_type实现。根据平台不同impl_type分为
n win_iocp_io_service Win版本的实现,这里主要分析Linux版本。
n task_io_service 非win平台下的实现,其代码结构为:
u detail/task_io_service_fwd.hpp 简单声明task_io_service名称
u detail/task_io_service.hpp 声明task_io_service的方法和属性
u detail/impl/task_io_service.ipp 具体实现文件
u 队列中的任务类型为opertioan,原型其实是typedef task_io_service_operation operation,其实现文件在detail/task_io_service_operation.hpp中,当队列中的任务被执行时,就是task_io_service_operation:: complete被调用的时候。
Post向队列中投递任务,然后激活空闲线程执行任务。其实现流程如下:
l Post接收handler作为参数,实际上是个仿函数,通过此仿函数构造出completion_handler对象,completion_handler继承自operation。然后调用post_immediate_completion。
l post_immediate_completion首先将outstanding_work_增加,然后调用post_deferred_completion。
l post_deferred_completion首先加锁将任务入列,然后调用wake_one_thread_and_unlock
l wake_one_thread_and_unlock尝试唤醒当前空闲的线程,其实现中特别之处在于,若没有空闲线程,但是有线程在执行task->run,即阻塞在epoll_wait上,那么先中断epoll_wait执行任务队列完成后再执行epoll_wait。
l first_idle_thread_维护了所有当前空闲线程,实际上使用了Leader/Follower模式,每次唤醒时只唤醒空闲线程的第一个。
Run方法执行队列中的所有任务,直到任务执行完毕。
l run方法首先构造一个idle_thread_info,和first_idle_thread_类型相同,即通过first_idle_thread_将所有线程串联起来,它这个串联不是立即串联的,当该线程无任务可做是加入到first_idle_thread_的首部,有任务执行时,从first_idle_thread_中断开。这很正常,因为first_idle_thread_维护的是当前空闲线程。
l 加锁,循环执行do_one方法,直到do_one返回false
l do_one每次执行一个任务。首先检查队列是否为空,若空将此线程追加到first_idle_thread_的首部,然后阻塞在条件变量上,直到被唤醒。
l 当被唤醒或是首次执行,若stopped_为true(即此时stop方法被调用了),返回0
l 队列非空,pop出一个任务,检查队列无任务那么简单的解锁,若仍有,调用wake_one_thread_and_unlock尝试唤醒其他空闲线程执行。然后执行该任务,返回1.
l 实际上在执行队列任务时有一个特别的判断if (o == &task_operation_),那么将会执行task_->run,task_变量类型为reactor,在linux平台实现为epoll_reactor,实现代码文件为detail/impl/epoll_reactor.ipp,run方法实际上执行的是epoll_wait,run阻塞在epoll_wait上等待事件到来,并且处理完事件后将需要回调的函数push到io_servie的任务队列中,虽然epoll_wait是阻塞的,但是它提供了interrupt函数,该interrupt是如何实现的呢,它向epoll_wait添加一个文件描述符,该文件描述符中有8个字节可读,这个文件描述符是专用于中断epoll_wait的,他被封装到select_interrupter中,select_interrupter实际上实现是eventfd_select_interrupter,在构造的时候通过pipe系统调用创建两个文件描述符,然后预先通过write_fd写8个字节,这8个字节一直保留。在添加到epoll_wait中采用EPOLLET水平触发,这样,只要select_interrupter的读文件描述符添加到epoll_wait中,立即中断epoll_wait。很是巧妙。!!!实际上就是因为有了这个reactor,它才叫io_servie,否则就是一个纯的任务队列了。
l Run方法的原则是:
n 有任务立即执行任务,尽量使所有的线程一起执行任务
n 若没有任务,阻塞在epoll_wait上等待io事件
n 若有新任务到来,并且没有空闲线程,那么先中断epoll_wait,先执行任务
n 若队列中有任务,并且也需要epoll_wait监听事件,那么非阻塞调用epoll_wait(timeout字段设置为0),待任务执行完毕在阻塞在epoll_wait上。
n 几乎对线程的使用上达到了极致。
n 从这个函数中可以知道,在使用ASIO时,io_servie应该尽量多,这样可以使其epoll_wait占用的时间片最多,这样可以最大限度的响应IO事件,降低响应时延。但是每个io_servie::run占用一个线程,所以io_servie最佳应该和CPU的核数相同。
l 加锁,调用stop_all_threads
l 设置stopped_变量为true,遍历所有的空闲线程,依次唤醒
l task_interrupted_设置为true,调用task_的interrupt方法
l task_的类型为reactor,在run方法中已经做了分析