###协程 协程,即协做式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工做,而其余协程处于休眠状态。协程能够在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行。 协程已经被证实是一种很是有用的程序组件,不只被python、lua、ruby等脚本语言普遍采用,并且被新一代面向多核的编程语言如golang rust-lang等采用做为并发的基本单位。 协程能够被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优势:python
###网络编程模型 咱们首先来简单回顾一下一些经常使用的网络编程模型。网络编程模型能够大致的分为同步模型和异步模型两类。react
同步模型使用阻塞IO模式,在阻塞IO模式下调用read等IO函数时会阻塞线程直到IO完成或失败。 同步模型的典型表明是thread_per_connection模型,每当阻塞在主线程上的accept调用返回时则建立一个新的线程去服务于新的socket的读/写。这种模型的优势是程序逻辑简洁,符合人的思惟;缺点是可伸缩性收到线程数的限制,当链接愈来愈多时,线程也愈来愈多,频繁的线程切换会严重拖累性能,同时不得不处理多线程同步的问题。git
异步模型通常使用非阻塞IO模式,并配合epoll/select/poll等多路复用机制。在非阻塞模式下调用read,若是没有数据可读则当即返回,并通知用户没有可读(EAGAIN/EWOULDBLOCK),而非阻塞当前线程。异步模型可使一个线程同时服务于多个IO对象。 异步模型的典型表明是reactor模型。在reactor模型中,咱们将全部要处理的IO事件注册到一个中心的IO多路复用器中(通常为epoll/select/poll),同时主线程阻塞在多路复用器上。一旦有IO事件到来或者就绪,多路复用器返回并将对应的IO事件分发到对应的处理器(即回调函数)中,最后处理器调用read/write函数来进行IO操做。github
异步模型的特色是性能和可伸缩性比同步模型要好不少,可是其结构复杂,不易于编写和维护。在异步模型中,IO以前的代码(IO任务的提交者)和IO以后的处理代码(回调函数)是割裂开来的。golang
###协程与网络编程 协程的出现出现为克服同步模型和异步模型的缺点,并结合他们的优势提供了可能: 如今假设咱们有3个协程A,B,C分别要进行数次IO操做。这3个协程运行在同一个调度器或者说线程的上下文中,并依次使用CPU。调度器在其内部维护了一个多路复用器(epoll/select/poll)。 协程A首先运行,当它执行到一个IO操做,但该IO操做并无当即就绪时,A将该IO事件注册到调度器中,并主动放弃CPU。这时调度器将B切换到CPU上开始执行,一样,当它碰到一个IO操做的时候将IO事件注册到调度器中,并主动放弃CPU。调度器将C切换到cpu上开始执行。当全部协程都被“阻塞”后,调度器检查注册的IO事件是否发生或就绪。假设此时协程B注册的IO时间已经就绪,调度器将恢复B的执行,B将从上次放弃CPU的地方接着向下运行。A和C同理。 这样,对于每个协程来讲,它是同步的模型;可是对于整个应用程序来讲,它是异步的模型。编程
好了,原理说完了,咱们来看一个实际的例子,echo server。ruby
###echo server网络
在这个例子中,咱们将使用orchid库来编写一个echo server。orchid库是一个构建于boost基础上的 协程/网络IO C++库。多线程
echo server首先必需要处理链接事件,咱们建立一个协程来专门处理链接事件:并发
typedef boost::shared_ptr<orchid::socket> socket_ptr; //处理ACCEPT事件的协程 void handle_accept(orchid::coroutine_handle co) { try { orchid::acceptor acceptor(co -> get_io_service());//构建一个acceptor acceptor.bind_and_listen("5678",true); for(;;) { socket_ptr sock(new orchid::socket(co -> get_scheduler().get_io_service())); acceptor.accept(*sock,co); //在调度器上建立一个协程来服务新的socket。 //第一个参数是要建立的协程的main函数, //第二个参数是要建立的协程的栈的大小。 co -> get_scheduler().spawn(boost::bind(handle_io,_1,sock),orchid::minimum_stack_size()); } } catch(orchid::io_error& e) { ORCHID_ERROR("id %lu msg:%s",co->id(),e.what()); } }
在orchid中,协程的main函数必须知足函数签名void(orchid::coroutine_handle),如handle_accept所示,其中参数co是协程句柄,表明了当前函数所位于的协程。
在上面的代码中,咱们建立了一个acceptor,并让它监听5678端口,而后在"阻塞"等待链接到来,当链接事件到来时,建立一个新的协程来服务新的socket。处理套接字IO的协程以下:
//处理SOCKET IO事件的协程 void handle_io(orchid::coroutine_handle co,socket_ptr sock) { orchid::buffered_reader<orchid::socket> reader(*sock,co,16);//在socket上构建缓冲输入流 orchid::buffered_writer<orchid::socket> writer(*sock,co,16);//在socket上构建缓冲输出流 try { std::string line; std::size_t n = 0; for(;;) { n = reader.read_until(line,'\n'); ORCHID_DEBUG("id %lu recv: %s",co->id(),line.c_str()); writer.write(line.c_str(),line.size()); writer.flush(); } } catch (const orchid::io_error& e) { if (e.code() == boost::asio::error::eof) { ORCHID_DEBUG("id %lu msg:%s",co->id(),"socket closed by remote side!"); } else { ORCHID_ERROR("id %lu msg:%s",co->id(),e.what()); } }
IO处理协程首先在传入的套接字上建立了一个输入流和一个输出流,分别表明了TCP的输入和输出。而后不断地从输入流中读取一行,并输出到输出流当中。当socket上的TCP链接断开时,会抛出orchid::io_error的异常,循环结束,值得注意的是eof事件也被当成异常来抛出。对于不喜欢使用异常的用户,orchid提供了另一套使用boost::system::error_code的接口。同时,对于熟悉asio的用户,orchid提供了一套boost asio风格的接口。
若是用户须要无缓冲的读写socket或者自建缓冲,能够直接调用orchid::socket的read和write函数,或者使用无缓冲的reader和writer。
细心的读者可能已经发现,handle_io的函数签名并不知足void(orchid::coroutine_handle),回到handle_accept中,能够发现,实际上咱们使用了boost.bind对handle_io函数进行了适配,使之符合函数签名的要求。
最后是main函数:
int main() { orchid::scheduler sche; sche.spawn(handle_accept,orchid::coroutine::minimum_stack_size());//建立协程 sche.run(); }
###总结 在上面这个echo server的例子中,咱们采用了一种 coroutine per connection 的编程模型,与传统的 thread per connection 模型同样的简洁清晰,可是整个程序实际上运行在同一线程当中,因此咱们也不须要处理多线程同步的问题。 因为协程的切换开销远远小于线程,所以咱们能够轻易的同时启动上千协程来同时服务上千链接,这是 thread per connection的模型很难作到的;在性能方面,整个底层的IO系统其实是使用boost.asio这种高性能的异步io库实现的。与IO所费的时间相比,协程切换的开销基本能够忽略。 所以经过orchid,咱们能够在保持同步IO模型简洁性的同时,得到异步IO模型的高性能和扩展性。
最后,若是您对orchid感兴趣,欢迎关注和参与。