构造函数
构造函数的主要动作就是调用CreateIoCompletionPort创建了一个初始iocp。
Dispatch和post的区别
Post一定是PostQueuedCompletionStatus并且在GetQueuedCompletionStatus 之后执行。
Dispatch会首先检查当前thread是不是io_service.run/runonce/poll/poll_once线程,如果是,则直接运行。
poll和run的区别
两者代码几乎一样,都是首先检查是否有outstanding的消息,如果没有直接返回,否则调用do_one()。唯一的不同是在调用size_t do_one(bool block, boost::system::error_code& ec)时前者block = false,后者block = true。
该参数的作用体现在:
BOOL ok = ::GetQueuedCompletionStatus(iocp_.handle, &bytes_transferred,
&completion_key, &overlapped, block ? timeout : 0);
因此可以看出,poll处理的是已经完成了的消息,也即GetQueuedCompletionStatus立刻能返回的。而run则会导致等待。
poll 的作用是依次处理当前已经完成了的消息,直到所有已经完成的消息处理完成为止。如果没有已经完成了得消息,函数将退出。poll不会等待。这个函数有点类似于PeekMessage。鉴于PeekMessage很少用到,poll的使用场景我也有点疑惑。poll的一个应用场景是如果希望handler的处理有优先级,也即,如果消息完成速度很快,同时可能完成多个消息,而消息的处理过程可能比较耗时,那么可以在完成之后的消息处理函数中不真正处理数据,而是把handler保存在队列中,然后按优先级统一处理。代码如下:
while (io_service.run_one()) {
// The custom invocation hook adds the handlers to the priority queue
// rather than executing them from within the poll_one() call.
while (io_service.poll_one()) ; pri_queue.execute_all(); }
循环执行poll_one让已经完成的消息的wrap_handler处理完毕,也即插入一个队列中,然后再统一处理之。这里的wrap_handler是一个class,在post的时候,用如下代码:
io_service.post(pri_queue.wrap(0, low_priority_handler));或者 acceptor.async_accept(server_socket, pri_queue.wrap(100, high_priority_handler));
template <typename Handler> wrapped_handler<Handler> handler_priority_queue::wrap(int priority, Handler handler)
{ return wrapped_handler<Handler>(*this, priority, handler); }
参见boost_asio/example/invocation/prioritised_handlers.cpp
这个sample也同时表现了wrap的使用场景。
也即把handler以及参数都wrap成一个object,然后把object插入一个队列,在pri_queue.execute_all中按优先级统一处理。
run的作用是处理消息,如果有消息未完成将一直等待到所有消息完成并处理之后才退出。
reset和stop
文档中reset的解释是重置io_service以便下一次调用。
当 run,run_one,poll,poll_one是被stop掉导致退出,或者由于完成了所有任务(正常退出)导致退出时,在调用下一次 run,run_one,poll,poll_one之前,必须调用此函数。reset不能在run,run_one,poll,poll_one正在运行时调用。如果是消息处理handler(用户代码)抛出异常,则可以在处理之后直接继续调用 io.run,run_one,poll,poll_one。 例如:
boost::asio::io_service io_service;
...
for (;;)
{
try
{
io_service.run();
break; // run() exited normally
}
catch (my_exception& e)
{
// Deal with exception as appropriate.
}
}在抛出了异常的情况下,stopped_还没来得及被asio设置为1,所以无需调用reset。
reset函数的代码仅有一行:
void reset()
{
::InterlockedExchange(&stopped_, 0);
}
也即,当io.stop时,会设置stopped_=1。当完成所有任务时,也会设置。
总的来说,单线程情况下,不管io.run是如何退出的,在下一次调用io.run之前调用一次reset没有什么坏处。例如:
for(;;)
{
try
{
io.run();
}
catch(…)
{
}
io.reset();
}
如果是多线程在运行io.run,则应该小心,因为reset必须是所有的run,run_one,poll,poll_one退出后才能调用。
文档中的stop的解释是停止io_service的处理循环。
此函数不是阻塞函数,也即,它仅仅只是给iocp发送一个退出消息而并不是等待其真正退出。因为poll和poll_one本来就不等待(GetQueuedCompletionStatus时timeout = 0),所以此函数对poll和poll_one无意义。对于run_one来说,如果该事件还未完成,则run_one会立刻返回。如果该事件已经完成,并且还在处理中,则stop并无特殊意义(会等待handler完成后自然退出)。对于run来说,stop的调用会导致run中的 GetQueuedCompletionStatus立刻返回。并且由于设置了stopped = 1,此前完成的消息的handlers也不会被调用。考虑一下这种情况:在io.stop之前,有1k个消息已经完成但尚未处理,io.run正在依次从 GetQueuedCompletionStatus中获得信息并且调用handlers,调用io.stop设置stopped=1将导致后许 GetQueuedCompletionStatus返回的消息直接被丢弃,直到收到退出消息并退出io.run为止。
void stop()
{
if (::InterlockedExchange(&stopped_, 1) == 0)
{
if (!::PostQueuedCompletionStatus(iocp_.handle, 0, 0, 0))
{
DWORD last_error = ::GetLastError();
boost::system::system_error e(
boost::system::error_code(last_error,
boost::asio::error::get_system_category()),
"pqcs");
boost::throw_exception(e);
}
}
}
注意除了让当前代码退出之外还有一个副作用就是设置了stopped_=1。这个副作用导致在stop之后如果不调用reset,所有run,run_one,poll,poll_one都将直接退出。
另一个需要注意的是,stop会导致所有未完成的消息以及完成了但尚未处理得消息都直接被丢弃,不会导致handlers倍调用。
注意这两个函数都不会CloseHandle(iocp.handle_),那是析构函数干的事情。
注意此处有个细节:一次PostQueuedCompletionStatus仅导致一次 GetQueuedCompletionStatus返回,那么如果有多个thread此时都在io.run,并且block在 GetQueuedCompletionStatus时,调用io.stop将PostQueuedCompletionStatus并且导致一个 thread的GetQueuedCompletionStatus返回。那么其他的thread呢?进入io_service的do_one(由run 函数调用)代码可以看到,当GetQueuedCompletionStatus返回并且发现是退出消息时,会再发送一次 PostQueuedCompletionStatus。代码如下:
else
{
// Relinquish responsibility for dispatching timers. If the io_service
// is not being stopped then the thread will get an opportunity to
// reacquire timer responsibility on the next loop iteration.
if (dispatching_timers)
{
::InterlockedCompareExchange(&timer_thread_, 0, this_thread_id);
}
// The stopped_ flag is always checked to ensure that any leftover
// interrupts from a previous run invocation are ignored.
if (::InterlockedExchangeAdd(&stopped_, 0) != 0)
{
// Wake up next thread that is blocked on GetQueuedCompletionStatus.
if (!::PostQueuedCompletionStatus(iocp_.handle, 0, 0, 0))
{
last_error = ::GetLastError();
ec = boost::system::error_code(last_error,
boost::asio::error::get_system_category());
return 0;
}
ec = boost::system::error_code();
return 0;
}
}
}
Wrap
这个函数是一个语法糖。
Void func(int a);
io_service.wrap(func)(a);
相当于io_service.dispatch(bind(func,a));
可以保存io_service.wrap(func)到g,以便在稍后某些时候调用g(a);
例如:
socket_.async_read_some(boost::asio::buffer(buffer_), strand_.wrap(
boost::bind(&connection::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred)));
这是一个典型的wrap用法。注意async_read_some要求的参数是一个handler,在read_some结束后被调用。由于希望真正被调用的handle_read是串行化的,在这里再post一个消息给io_service。以上代码类似于:
void A::func(error,bytes_transferred)
{
strand_.dispatch(boost::bind(handle_read,shared_from_this(),error,bytes_transferred);
}
socket_.async_read_some(boost::asio::buffer(buffer_), func);
注意1点:
io_service.dispatch(bind(func,a1,…an)),这里面都是传值,无法指定bind(func,ref(a1)…an)); 所以如果要用ref语义,则应该在传入wrap时显式指出。例如:
void func(int& i){i+=1;}
void main()
{
int i = 0;
boost::asio::io_service io;
io.wrap(func)(boost::ref(i));
io.run();
printf("i=%d\n");
}
当然在某些场合下,传递shared_ptr也是可以的(也许更好)。
从handlers抛出的异常的影响
当handlers抛出异常时,该异常会传递到本线程最外层的io.run,run_one,poll,poll_one,不会影响其他线程。捕获该异常是程序员自己的责任。
例如:
boost::asio::io_service io_service;
Thread1,2,3,4()
{
for (;;)
{
try
{
io_service.run();
break; // run() exited normally
}
catch (my_exception& e)
{
// Deal with exception as appropriate.
}
}
}
Void func(void)
{
throw 1;
}
Thread5()
{
io_service.post(func);
}
注意这种情况下无需调用io_service.reset()。
这种情况下也不能调用reset,因为调用reset之前必须让所有其他线程正在调用的io_service.run退出。(reset调用时不能有任何run,run_one,poll,poll_one正在运行)
Work
有些应用程序希望在没有pending的消息时,io.run也不退出。比如io.run运行于一个后台线程,该线程在程序的异步请求发出之前就启动了。
可以通过如下代码实现这种需求:
main()
{
boost::asio::io_service io_service;
boost::asio::io_service::work work(io_service);
Create thread
Getchar();
}
Thread()
{
Io_service.run();
}
这种情况下,如果work不被析构,该线程永远不会退出。在work不被析构得情况下就让其退出,可以调用io.stop。这将导致 io.run立刻退出,所有未完成的消息都将丢弃。已完成的消息(但尚未进入handler的)也不会调用其handler函数(由于在stop中设置了 stopped_= 1)。
如果希望所有发出的异步消息都正常处理之后io.run正常退出,work对象必须析构,或者显式的删除。
boost::asio::io_service io_service;
auto_ptr<boost::asio::io_service::work> work(
new boost::asio::io_service::work(io_service));
...
work.reset(); // Allow run() to normal exit.
work是一个很小的辅助类,只支持构造函数和析构函数。(还有一个get_io_service返回所关联的io_service)
代码如下:
inline io_service::work::work(boost::asio::io_service& io_service)
: io_service_(io_service)
{
io_service_.impl_.work_started();
}
inline io_service::work::work(const work& other)
: io_service_(other.io_service_)
{
io_service_.impl_.work_started();
}
inline io_service::work::~work()
{
io_service_.impl_.work_finished();
}
void work_started()
{
::InterlockedIncrement(&outstanding_work_);
}
// Notify that some work has finished.
void work_finished()
{
if (::InterlockedDecrement(&outstanding_work_) == 0)
stop();
}
可以看出构造一个work时,outstanding_work_+1,使得io.run在完成所有异步消息后判断outstanding_work_时不会为0,因此会继续调用GetQueuedCompletionStatus并阻塞在这个函数上。
而析构函数中将其-1,并判断其是否为0,如果是,则post退出消息给GetQueuedCompletionStatus让其退出。
因此work如果析构,则io.run会在处理完所有消息之后正常退出。work如果不析构,则io.run会一直运行不退出。如果用户直接调用io.stop,则会让io.run立刻退出。
特别注意的是,work提供了一个拷贝构造函数,因此可以直接在任意地方使用。对于一个io_service来说,有多少个work实例关联,则outstanding_work_就+1了多少次,只有关联到同一个io_service的work全被析构之后,io.run才会在所有消息处理结束之后正常退出。
strand
strand是另一个辅助类,提供2个接口dispatch和post,语义和io_service的dispatch和post类似。区别在于,同一个strand所发出的dispatch和post绝对不会并行执行,dispatch和post所包含的handlers也不会并行。因此如果希望串行处理每一个tcp连接,则在accept之后应该在该连接的数据结构中构造一个strand,并且所有dispatch/post(recv /send)操作都由该strand发出。strand的作用巨大,考虑如下场景:有多个thread都在执行async_read_some,那么由于线程调度,很有可能后接收到的包先被处理,为了避免这种情况,就只能收完数据后放入一个队列中,然后由另一个线程去统一处理。
void connection::start()
{
socket_.async_read_some(boost::asio::buffer(buffer_),
strand_.wrap(
boost::bind(&connection::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred)));
}
不使用strand的处理方式:
前端tcp iocp收包,并且把同一个tcp连接的包放入一个list,如果list以前为空,则post一个消息给后端vnn iocp。后端vnn iocp收到post的消息后循环从list中获取数据,并且处理,直到list为空为止。处理结束后重新调用 GetQueuedCompletionStatus进入等待。如果前端tcp iocp发现list过大,意味着处理速度小于接收速度,则不再调用iocpRecv,并且设置标志,当vnn iocp thread处理完了当前所有积压的数据包后,检查这个标志,重新调用一次iocpRecv。
使用strand的处理方式:
前端tcp iocp收包,收到包后直接通过strand.post(on_recved)发给后端vnn iocp。后端vnn iocp处理完之后再调用一次strand.async_read_some。
这两种方式我没看出太大区别来。如果对数据包的处理的确需要阻塞操作,例如db query,那么使用后端iocp以及后端thread是值得考虑的。这种情况下,前端iocp由于仅用来异步收发数据,因此1个thread就够了。在确定使用2级iocp的情况下,前者似乎更为灵活,也没有增加什么开销。
值得讨论的是,如果后端多个thread都处于db query状态,那么实际上此时依然没有thread可以提供数据处理服务,因此2级iocp意义其实就在于在这种情况下,前端tcp iocp依然可以accept,以及recv第一次数据,不会导致用户connect不上的情况。在后端thread空闲之后会处理这期间的recv到的数据并在此async_read_some。
如果是单级iocp(假定handlers没有阻塞操作),多线程,那么strand的作用很明显。这种情况下,很明显应该让一个tcp连接的数据处理过程串行化。
Strand的实现原理
Strand内部实现机制稍微有点复杂。每次发出strand请求(例如 async_read(strand_.wrap(funobj1))),strand再次包裹了一次成为funobj2。在async_read完成时,系统调用funobj2,检查是否正在执行该strand所发出的完成函数(检查该strand的一个标志位),如果没有,则直接调用 funobj2。如果有,则检查是否就是当前thread在执行,如果是,则直接调用funobj2(这种情况可能发生在嵌套调用的时候,但并不产生同步问题,就像同一个thread可以多次进入同一个critical_session一样)。如果不是,则把该funobj2插入到strand内部维护的一个队列中。