asio中异步IO的体会

  想在QT的程序中使用asio库实现异步IO操作,服务端是按照asio的http范例改写的,没什么问题.而客户端基本上都是采用同步方式.那么我能不能在客户端采用异步方式呢?第一感觉是肯定可以,而且会很简单的就能实现.

  那就将客户端的IO操作改写为异步的吧.首先在客户端的主界面类中声明一个io_service指针,而后在主界面类的构造函数中初始化并调用其run()方法,在按钮的事件处理函数中调用async_read方法.但怎么都不触发异步操作.同样的代码改为控制台的方式,启动了异步操作后在调用run()方法则一切正常.后来就在按钮的click事件中创建io_service,也先启动异步操作,在调用run()方法,一起正常了.查找asio的文档,说如果没有需要处理的异步操作,run()方法就返回了,我原来的方式是在构造函数中调用的run(),前面没有任何的异步操作,当然run()方法马上返回了,后面在调用异步操作,都没人理睬了,问题就是出自这里.调整的方案每次点击都创建一个io_service实例有些丑陋.查找了一下资料后,发现有人遇到过这种问题,并提供了解决方案.请见http://blog.csdn.net/fengge8ylf/article/details/6796175

为了以后阅读方便,将帖子转帖如下:

解决io_service::run在没有任务就返回的问题

 当有任务的时候,run函数会一直阻塞;但当没有任务了,run函数会返回,所有异步操作终止。

在一个客户端程序中,如果我想连接断开后重连,由于连接断开了,run会返回,当再次重连的时候,由于run返回了,即使连接成功了,也不会调用aysnc_connect绑定的回调函数。

有两个解决方法。

1,在再次重连的时候,要重新调用run函数,在调用的前一定要调用io_service::reset。以便io_service::run重用。

2,用boost::asio::io_service::work。

    boost::asio::io_service io_service_;

    boost::asio::io_service::work work(io_service_);

    io_service_.run();

  这样即使没有任务,run也不会返回了。

  现在回想一下http范例的服务端代码,是这样的一个流程:首先启动一个socket的异步接收函数,如果接收到了一个客户端的连接,创建一个session类的实例,并进行一系列的异步操作,接着又启动了一个Socket的异步接收函数.如此反复,保证服务端总有一个异步接收动作在等待客户端连接,使io_service::run()函数永远不会退出.而客户端则不同,连接的发起及与服务端通信都是由用户界面触发的,如果要进行异步操作,必须确保io_service::run()方法一直有需要处理的异步操作,或使用其他方法让run()方法不会在空闲时退出,或重新启动run()方法.

  那么在看一下异步触发函数是由那个线程执行的,asio文档说异步操作会创建几个工作线程用来等待异步操作完成,完成后提取到异步操作处理函数,交给run()执行.那么当然是哪个线程调用了run()函数,哪个线程就会执行异步操作处理函数了.对此进行了验证,分别在按钮事件中和异步操作处理函数中输出了当前线程ID,果然一样.

  DWORD tid = GetCurrentThreadId();
  QString str = tr("%1").arg(tid);
  qDebug() << str;

  前段时间翻译了asio的文档,自以为对asio已经有了初步的理解了,现在看来还差得远.找时间多练习练习吧.

【转】boost asio io_service学习笔记

构造函数

构造函数的主要动作就是调用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 wrapped_handler handler_priority_queue::wrap(int priority, Handler handler)
{    return wrapped_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 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内部维护的一个队列中。

你可能感兴趣的:(跨平台-QT,C/C++)