本文继续上一篇 WebServer 高性能服务器_沐雨橙风24的博客-优快云博客,主要增加线程池和定时器功能,大幅度提高服务器性能。代码地址 增加线程池及定时器
本系列文章:
C++11的多线程
传统的C++(C++11标准之前)中并没有引入线程这个概念,在C++11出来之前,如果我们想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthread.h>。C++11提供了语言层面上的多线程,包含在头文件<thread>中。它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。
当线程启动后,一定要在和线程相关联的 thread 销毁前,确定以何种方式等待线程执行结束。
detach 方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。
join 方式,等待启动的线程完成,才会继续往下执行。
创建 lock_guard 对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开 lock_guard 对象的作用域时,lock_guard 析构并释放互斥量。lock_guard 的特点:
创建即加锁,作用域结束自动析构并解锁,无需手工解锁
不能中途解锁,必须等作用域结束才解锁
不能复制
std::unique_lock 对象以独占所有权的方式(unique owership)管理 mutex 对象的上锁和解锁操作,即在 unique_lock 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 unique_lock 的生命周期结束之后,它所管理的锁对象会被解锁。unique_lock 具有lock_guard 的所有功能,而且更为灵活。
当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_ 唤醒了当前线程。在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_ 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。
添加定时器和线程池
线程池
任务量小时,可以一个任务对应一个线程,线程资源的创建与销毁的时间消耗、上下文切换资源占用都可以忽略。但是在高并发、高吞吐、低延迟的场景下,就必须降低线程资源管理的时间消耗,就需要使用线程池了。
线程池的基本思想就是:创建若干个线程执行死循环函数,当任务队列中没有任务时通过调用 wait 函数阻塞线程,当添加任务时使用 notify_one 唤醒一个线程执行函数,需要注意在操作任务队列时需要使用锁来防止多线程操作同一个任务。
class ThreadPool {
public:
explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared<Pool>()) {
assert(threadCount > 0);
for(size_t i = 0; i < threadCount; i++) { // 创建threadCount个线程
std::thread([pool = pool_] { // 使用lambda表达式
std::unique_lock<std::mutex> locker(pool->mtx); // 加锁,防止多个线程取到同一个任务
while(true) { // 采用死循环
if(!pool->tasks.empty()) {
auto task = std::move(pool->tasks.front());
pool->tasks.pop();
locker.unlock(); // 对任务队列操作结束释放锁
task(); // 执行函数
locker.lock(); // 函数执行完毕,重新加锁
}
else if(pool->isClosed) break;
else pool->cond.wait(locker); // 线程阻塞等待被唤醒
}
}).detach();
}
}
ThreadPool() = default;
ThreadPool(ThreadPool&&) = default;
~ThreadPool() {
if(static_cast<bool>(pool_)) {
{
std::lock_guard<std::mutex> locker(pool_->mtx);
pool_->isClosed = true;
}
pool_->cond.notify_all();
}
}
template<class F>
void AddTask(F&& task) {
{
std::lock_guard<std::mutex> locker(pool_->mtx); // 加入队列时加锁
pool_->tasks.emplace(std::forward<F>(task)); // 将任务添加到队列中
}
pool_->cond.notify_one(); // 唤醒一个线程
}
private:
struct Pool {
std::mutex mtx; // 创建锁
std::condition_variable cond; // 创建条件变量
bool isClosed = false; // 判断是否关闭线程
std::queue<std::function<void()>> tasks; // 请求任务队列
};
std::shared_ptr<Pool> pool_;
};
基于小根堆实现的定时器,关闭超时的非活动连接
由于堆是一种经过排序的完全二叉树,因此在构建的时候需要对新插入的节点进行一些操作以使其符合堆的性质。这种操作就是节点的上滤与下滤。以最小堆为例:
上滤: 将当前节点与其父节点相比,如果当前节点的值比较小,就把当前节点与父节点交换,继续前面的比较,知道当前节点的值比父节点的值大为止。此时,便符合最小堆的定义。
void HeapTimer::siftup_(size_t i) {
assert(i >= 0 && i < heap_.size());
size_t j = (i - 1) / 2;
while(j >= 0) {
if(heap_[j] < heap_[i]) { break; }
SwapNode_(i, j);
i = j;
j = (i - 1) / 2;
}
}
下滤: 将当前节点与其左、右子节点相比,如果当前节点的值比其中一个(或两个)子节点的值大,就把当前节点与两个子节点中较小的那个交换,继续前面的比较,知道当前节点的值比两个子节点的值都小为止。此时,便符合最小堆的定义。
bool HeapTimer::siftdown_(size_t index, size_t n) { // 从index到n之间进行下滤
assert(index >= 0 && index < heap_.size());
assert(n >= 0 && n <= heap_.size());
size_t i = index;
size_t j = i * 2 + 1;
while(j < n) {
if(j + 1 < n && heap_[j + 1] < heap_[j]) j++;
if(heap_[i] < heap_[j]) break;
SwapNode_(i, j);
i = j;
j = i * 2 + 1;
}
return i > index;
}
删除:交换要删除节点和最后一个节点,然后进行下滤和上滤操作,最后删除数组尾部元素
void HeapTimer::del_(size_t index) {
/* 删除指定位置的结点 */
assert(!heap_.empty() && index >= 0 && index < heap_.size());
/* 将要删除的结点换到队尾,然后调整堆 */
size_t i = index;
size_t n = heap_.size() - 1;
assert(i <= n);
if(i < n) {
SwapNode_(i, n);
if(!siftdown_(i, n)) {
siftup_(i);
}
}
/* 队尾元素删除 */
ref_.erase(heap_.back().id);
heap_.pop_back();
}
定时器用于定期检测一个客户连接的活动状态,删除不活跃的客户连接,提高服务器的性能。
定义一个时间堆节点用于表示一个客户连接
struct TimerNode {
int id;
TimeStamp expires; // 过期时间
TimeoutCallBack cb; // 超时回调函数
bool operator<(const TimerNode& t) const { // 重载<符号,用于堆排序使用
return expires < t.expires;
}
};
需要注意的是在定义 HeapTimer 类的时候,利用哈希表定义了一个索引,在查找节点的时候时间复杂度为O(1)
std::unordered_map<int, size_t> ref_; // 存放节点在数组中对应位置,保证查询时间复杂度O(1)
HeapTimer 类中几个重要的接口
void HeapTimer::adjust(int id, int timeout) {
/* 调整指定id的结点 */
assert(!heap_.empty() && ref_.count(id) > 0);
heap_[ref_[id]].expires = Clock::now() + MS(timeout);;
siftdown_(ref_[id], heap_.size());
}
void HeapTimer::add(int id, int timeOut, const TimeoutCallBack &cb) {
assert(id >= 0);
size_t i;
if(ref_.count(id) == 0) { /* 新节点:堆尾插入,调整堆 */
i = heap_.size();
ref_[id] = i;
heap_.push_back({id, Clock::now() + MS(timeOut), cb});
siftup_(i);
}
else { /* 已有结点:调整堆 */
i = ref_[id];
heap_[i].expires = Clock::now() + MS(timeOut);
heap_[i].cb = cb;
if(!siftdown_(i, heap_.size())) {
siftup_(i);
}
}
}
void HeapTimer::tick() {
/* 清除超时结点 */
if(heap_.empty()) {
return;
}
while(!heap_.empty()) {
TimerNode node = heap_.front();
// 使用std::chrono::duration_cast进行时间转换
if(std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) {
break;
}
node.cb();
pop();
}
}
int HeapTimer::GetNextTick() { // 得到下一个最小的超时时间
tick();
size_t res = -1;
if(!heap_.empty()) {
res = std::chrono::duration_cast<MS>(heap_.front().expires - Clock::now()).count();
if(res < 0) { res = 0; }
}
return res;
}
使用Webbench 测试,目前已经可以达到上万并发连接:

到这里,我们就基本实现了一个高性能的 HTTP 服务器,其中包括的技术点:
自动增长的缓冲区
利用状态机和正则表达式解析 HTTP 请求报文
I/O 复用技术 epoll
利用线程池实现多线程的 Reactor 高并发模型
基于小根堆实现的定时器,关闭超时的非活动连接
后续将实现注册和登录功能,同时添加数据库连接池提高性能,最后完善日志系统。