首先我们先了解几个问题:
如果指的是“并发连接数”,那么不能。
假如单纯采用 thread per connection 的模型,对于32位 linux,一个进程的地址空间是4G,其中用户态能访问3G左右,而一个线程的默认栈大小为10M,心算可以得到,一个进程大约最多能同时启动300个线程左右,这远远低于基于事件单线程程序所能轻松达到的并发连接数(几千上万,甚至几万)。所谓“基于事件”,指的是用 I/O mutiplexing event loop
的编程模型,又称为 Reactor 模式。
对于计算密集型服务,不能
如果在一个8核的机器上压缩100个 1G 的文本文件,每个core的处理能力为 200MB/s,那么“每次起8个进程,一个进程压缩一个文件”与“只启动一个进程(8个线程并发压缩一个文件)”,这两种方式总耗时相当,但是第二种方式能较快的拿到第一个压缩完的文件。
多线程程序如何让 I/O 和计算重叠,降低 latency (迟延)
例如:日志(logging),多个线程写日志,由于文件操作比较慢,服务线程会等在 IO上,让 CPU 空闲,增加响应时间。
解决办法:单独用一个 logging 线程负责写磁盘文件,通过 BlockingQueue 提供对外接口,别的线程要写日志的时候往队列一塞就行,这样服务线程的计算和logging线程的磁盘 I/O 就可以重叠了。
如果池中执行任务时,密集计算所占时间比重为P(0 < P <= 1),而系统一共有 C 个CPU,为了让 C 个 CPU跑满而不过载,线程池大小的经验公式 T=C/P。
假设 C = 8,P = 0.5,线程池的任务有一半是计算,一半是 IO,那么T = 16,也就是16个“50%繁忙的线程”能让 8 个 CPU忙个不停。
什么是Reactor? 换个名词“non-blocking IO + IO multiplexing”,意思就显而易见了。Reactor模式用 非阻塞IO + poll(epoll)函数来处理并发,程序的基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑。
while(!done)
{
int retval = poll(fds,nfds,timeout)
if(retval < 0)
处理错误,回调用户的error handler
else{
处理到期的timers,回调用户的timer handler
if(retval > 0){
处理IO事件,毁掉用户的IO event handler
}
}
}
这段代码形式上非常简单,跟我上一篇文章epoll的例子十分相似,除了没有处理超时timer部分。在muduo的实现中,定时器使用了linux平台的timerfd_*系列函数, timers和其它IO统一了起来。
muduo的Reactor核心主要由Channel、EventLoop、Poller、TimerQueue这几个类完成。乍一看还有一点绕,代码里面各种回掉函数看起来有点不直观。另外,这几个类的生命周期也值得注意,容易理不清楚。
1:Channel 类
Channel类比较简单,负责IO事件分发,每一个Channel对象都对应了一个fd,它的核心成员如下:
EventLoop* loop_;
const int fd_;
int events_;
int revents_;
int index_;
ReadEventCallback readCallback_;
EventCallback writeCallback_;
EventCallback errorCallback_;
EventCallback closeCallback_;
几个callback函数都是c++新标准里面的function对象(muduo里面是Boost::function),它们会在 handleEvent 这个成员函数中根据不同的事件被调用。index_是 poller 类中 pollfds_数组的下标。events_和 revents_明显对应了 struct pollfd 结构中的成员。需要注意的是,Channel 并不拥有该 fd,它不会在析构函数中去关闭这个 fd( fd是由Socket类的析构函数中关闭,即RAII的方法),Channel 的生命周期由其 owner 负责。
2.Poller类
Poller类在这里是poll函数的封装(在muduo源码里面是抽象基类,支持poll和epoll),它有两个核心的数据成员:
typedef std::vector<struct pollfd> PollFdList;
typedef std::map<int, Channel*> ChannelMap; // fd to Channel
PollFdList pollfds_;
ChannelMap channels_;
ChannelMap是fd到Channel类的映射,PollFdList保存了每一个fd所关心的事件,用作参数传递到poll函数中,Channel类里面的index_即是这里的下标。Poller类有下面四个函数
Timestamp poll(int timeoutMs, ChannelList* activeChannels);
void updateChannel(Channel* channel);
void removeChannel(Channel* channel);
private:
void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;
updateChannel
和 removeChannel
都是对上面两个数据结构的操作,poll函数是对::poll的封装。私有的 fillActiveChannels
函数负责把返回的活动时间添加到 activeChannels
(vectorPoller的生命周期和 EventLoop一样长
。
3.EventLoop类
EventLoop类是核心,大多数类都会包含一个EventLoop*的成员,因为所有的事件都会在EventLoop::loop()中通过Channel分发。先来看一下这个loop循环:
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
//++iteration_;
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
// TODO sort channel by priority
eventHandling_ = true;
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
doPendingFunctors();
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}
handleEvent 是 Channel 类的成员函数,它会根据事件的类型去调用不同的 Callback。循环末尾还有一个doPendingFunctors(),这个函数的作用在后面多线程的部分去说明。
由上面三个类已经可以构成 Reactor 的核心,整个流程如下:
muduo的书里面有一个时序图(8-1),很清楚的说明了整个流程。