cpp项目 ---ranxin_tinyhttpd

这篇博客主要介绍一下引入的线程池机制

以及对应使用到的C++11的新特性

ThreadPool

上一篇CPP学习日记讲到了,我们再来复盘一下

首先,我们自定义一个线程池的目的是为了减少线程池的创建和销毁带来的开销

其实现的主要原理有以下几点:

  • 任务队列存储了需要交给线程池处理的任务
  • 工作线程等待任务队列对其唤醒
  • 使用互斥锁和条件变量实现同步机制

入队函数

设计思路:

这是一个模板类,同时为了防止返回类型出错,我们告诉编译器我要返回的类型是一个future类

template<class F,class ... Args> // 传入的函数类型、参数类型
	auto enqueue(F&& f,Args&& ... args)
    -> std::future<typename std::result_of<F(Args...)>::type

写个返回类型别名,方便后续调用

同时用共享指针封装一个future类的共享指针

异步获取future返回值

using return_type = typename std::result_of<F(Args...)>::type
    
auto task = make_shared<std::packaged_task<return_type()>>(
	std::bind(std::forward<F>(f),std::forward<Args>(args)...)
)
std::future res = task->get_future();

等锁,上锁,然后入队

{
    std::unique_lock<std::mutex> u_lock(queue_mutex);
    if(stop)throw std::runtime_error("STOP!");
    tasks.emplace([task]{(*task)()});
}

然后根据任务队列情况、利用条件变量通知队列

最后返回res

if(tasks.size()>4){ // one 还是 all 取决于负载的动态变化 
    condition.notify_one();
}else{
    condition.notify_all();
} 
return res;

Q:为什么要确定或者返回res? 我不是没有需要返回的吗?

A:在服务器处理连接的时候确实没用到返回值,因为传入的lambda表达式没用返回值

这意味着server每次处理连接,响应结果传给client时,自己是不知道返回结果的,可以在后续加入状态位,以保证服务器也知道响应结果

构造函数

循环迭代构建线程,注意:由于我们的动态数组对象是线程,这样我们可以直接在动态数组中传入lambda表达式,线程会自动与其绑定并执行

workers.emplace_back(
	[this]{
        while(true){
            std::function<void()> task;
            {// 取锁并等待
                std::unique<std::mutex> u_lock(this->queue_mutex);
                this->condition.wait(u_lock,
                [this]{return !this->tasks.empty()&&this->stop});
                if(stop)return;
                task = tasks.front();tasks.pop();
            }
            task();//执行调用
        }
    }
)

std::function<void()> task;这里不需要参数,因为队列中bind已经将其打包好,而且packaged_task中接收的return_type()括号中也是空的

条件变量的wait函数不仅可以只传入锁,也可以同时传入谓词

以避免假唤醒(唤醒之后没任务做)

析构函数

析构函数中比较简单,我们需要做的就是取锁,等锁空下来我们更新stop位

然后通知所有线程,最后等待所有线程结束

这一步看上去上锁是没必要的,因为我的线程池还过于简单

没有出现为了处理中间线程池的异常而停止整个线程池的情况

如果有了对应情况就应该保证共有变量stop线程安全

TODO

  • 编写Logger类保存日志
  • 写个连接池方便动态扩容
  • 根据连接数量或者连接频率动态扩容
  • 利用事件循环机制(select、poll、epoll)实现I/O多路复用

ISSUE

  • POST带请求体无法传递给管道

Q&A

  • Q: 为什么需要内存对齐?

    A: 抛开其他硬件要求不谈,主要是为了提高存取效率,你看我们正常地遍历一个数组,都是通过下标递增遍历的,在机器内部呢,一个int占了4个字节,所以要4个字节4个字节的往下遍历,这就是正常内存对齐的方案

    可如果我们创建了一个包含char 和 int 的结构体,那么我们的存取的时候,是不是没办法再4个4个遍历了?

    所以为了更好地提高存取效率,我们会在char 后面 填充3个字节

    当然,我们也可以在自己设计时,先将占用字节少的成员放在前面,这样可以再次减少填充带来的开销

  • Q: 线程之间有哪些通信机制?

    A: 互斥锁,很像进程之间通信的信号量机制,保证了线程可以独占某个资源,其他线程想要使用时必须等待,一般与条件变量一同使用,因为解锁需要通知其他线程,平时则需要等待锁

    读写锁,和互斥锁实现原理一致,但允许在读取时多个线程同时读取,写操作与互斥锁一致

    future和promise可以相互关联,promise可以设置触发的值或异常,future则用来等待并捕获这个值或异常

    atomic模板类可以实现线程安全,保证每次线程操作都符合原子性

  • Q: Lambda表达式实现的底层原理是什么?

    A: 为该表达式生成一个唯一的类(或结构体)

    Lambda表达式捕获的值或引用会作为该类的成员变量

    调用相应的构造函数来初始化

    然后重载()操作符函数,传入Lambda表达式()中的参数,然后执行lambda表达式{}中的内容

    auto lambda = [x](int y) { return x + y; };
    

    生成对应的类

    class UniqueLambdaClassName {
        int captured_x;
    public:
        UniqueLambdaClassName(int x) : captured_x(x) {}
        int operator()(int y) const {
            return captured_x + y;
        }
    };
    
  • Q: 为什么拷贝构造函数一般返回引用而不是值?

  • A: 因为返回引用可以减少副本拷贝的成本

    同时可以进行链式赋值,如果我返回的是值而不是引用,就调用不了操作符=函数了,因为该函数接收进来的是一个引用

参考资料

Tinyhttpd仓库(fork的)

socket库文档

地址库文档

附上本项目仓库

文章作者: P4ul
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 打工人驿站
后端 网络编程 c++ github
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝