分类目录归档:网络编程

协程与异步 IO(转载)

本文转载自: http://dongliu84.appspot.com/post/5906310176440320

这篇文章解释了我心中很多关于协程的疑问。

以下为正文。

协程(coroutine)的概念已经广为人知,这里就不多说了。作为用户态主动调度的执行单位,协程非常的轻量,并且调度是协作式的,协程可以避免传统多线程程序的上下文切换、调度和锁竞争等开销。

很多人都有这个误解,认为有了协程,就可以用同步程序的方式,写出异步的程序,原先同步的程序和第三方库,也会自动变成异步的。为什么说这是个误解呢,因为要写出有异步效果的程序,只有协程是不够的,还需要有底层IO的支持。协程的调度原本是编程者手工来做的,但是和异步IO结合的话,在发生IO时,自动将IO操作交给异步实现去执行,并让渡出协程的执行权,由调度器去调度执行其他协程。

除去天生支持协程或变种协程的lua, stackless-python, golang 之外,一个本不支持协程的语言也可以通过各种变通方法,来提供对协程的支持。事实上,基本上所有的的语言都有对协程支持的第三方实现,如 Java 的 Kilim, c++ 的boost.coroutine, Python 的 eventlet 和 gevent 等。

问题是,这些语言的底层IO实现,并未对协程调用做处理,其结果是仍然会阻塞这个协程,并没有实现异步的效果。而常用的http /数据库/缓存等lib 都是基于这些语言的底层IO库实现的(如httpclient/libcurl/mysql-connector等),所以使用了这些库的话,在IO 操作时不会让渡执行权,在当前协程阻塞在IO操作上的时候,其他协程也完全无法执行,这比多线程的实现还要糟糕。

除了 IO 之外,第三方lib 如果使用了线程,或是使用了锁,信号量等线程下的同步机制,或是使用了同步的Queue等,在协程环境下使用的时候也会出现各种问题。

所以,java, c++这样的语言,虽然有协程的实现,但是使用范围并不广,因为协程不能利用原先已经存在的大量lib ,需要按照协程的方式重新实现一遍。

一个例外是Python。 Python这样的动态语言,可以使用monkey patch的方式替换系统的IO / thread / queue / lock 等实现, 将其替换成对应的协程。Gevent就是这种做法。

而lua,stackless, golang 这样天生支持协程的语言,所有的IO lib 都是协程实现的,自然可以放心使用。

虽然非常简单,使用协程依然有需要注意的地方。比如:

协程不能有同步IO, 但并非所有的IO操作都一定是异步的。例如文件IO、Pipe、终端输入输出、DNS 解析等,经常没有异步的实现,通常的实现会用线程池进行一个封装,但依然需要在意。
协程只有在发生IO的时候才会让渡执行权,因此存在调度公平性的问题。在大量cpu操作、没有IO的情况下,当前协程会一直占用执行,其他协程得不到执行的机会。所以协程要注意不要有大量密集CPU操作。
协程的开销。协程依然需要占用一定的内存,如goroutine 目前是最小4k 的空间占用。协程切换的代价比较小,但不等于没有,比如stackfull 的协程实现在协程切换是需要做context copy,也有一定的开销。相比较纯异步API 编写的程序,协程的效率通常会差一些。
关于goroutine

goroutine的实现并不完全是传统意义上的协程。在协程阻塞的时候(cpu计算或者文件IO等),多个goroutine会变成多线程的方式执行。golang1.2之后还有类似erlang reduction的机制,来改善goroutine调度的公平性。这个机制只有在函数调用等场合下才会生效,所以效果还比较有限。

Libevent(一)

一、什么是libevent

libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。著名分布式缓存软件memcached也是基于libevent,而且libevent在使用上可以做到跨平台,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能。——引用自百度百科

需要注意以下几点:

  1. libevent是一个事件触发的网络库
  2. 可以跨平台(windows、linux、bsd等)
  3. 使用select、epoll(linux)、kqueue(freebsd)、IOCP(windows)等系统调用管理事件机制(IO复用)
  4. libevent在Linux环境下默认采用epoll作为IO多路复用方法

用户线程使用libevent则通常按以下步骤:

  1. 用户线程通过event_init()函数创建一个event_base对象。event_base对象管理所有注册到自己内部的IO事件。多线程环境下,event_base对象不能被多个线程共享,即一个event_base对象只能对应一个线程。
  2. 然后该线程通过event_add函数,将与自己感兴趣的文件描述符相关的IO事件,注册到event_base对象,同时指定事件发生时所要调用的事件处理函数(event handler)。服务器程序通常监听套接字(socket)的可读事件。比如,服务器线程注册套接字sock1的EV_READ事件,并指定event_handler1()为该事件的回调函数。libevent将IO事件封装成struct event类型对象,事件类型用EV_READ/EV_WRITE等常量标志。
  3. 注册完事件之后,线程调用event_base_loop进入循环监听(monitor)状态。该循环内部会调用epoll等IO复用函数进入阻塞状态,直到描述符上发生自己感兴趣的事件。此时,线程会调用事先指定的回调函数处理该事件。例如,当套接字sock1发生可读事件,即sock1的内核buff中已有可读数据时,被阻塞的线程立即返回(wake up)并调用event_handler1()函数来处理该次事件。
  4. 处理完这次监听获得的事件后,线程再次进入阻塞状态并监听,直到下次事件发生。

—— 引用自Memcached网络模型

二、php使用libevent扩展实现高性能服务器

此处不再赘述php-libevent扩展的安装。
上述在描述libevent步骤时候,用的并非php函数,php实现是略微有区别,大致流程不变。

在之前的文章『php-socket 实现简单服务器』中,我们使用stream+select的方式来实现一个简单的服务器。

现在我们就使用libevnet来实现更高性能的服务器。

附上libevent的常量