tinyWebServer 学习笔记——一、半同步半反应堆线程池

news2025/1/21 0:55:48

文章目录

  • 一、基础知识
    • 1. 服务器编程基本框架
    • 2. 五种 I/O 模型
    • 3. 事件处理模式
    • 4. 半同步/半反应堆模式
    • 5. 线程池
  • 二、代码解析
    • 1. 线程池类定义
    • 2. 线程池创建与回收
    • 3. 向请求队列中添加任务
    • 4. 线程处理函数
    • 5. run 执行任务
  • 参考文献

一、基础知识

1. 服务器编程基本框架

1

服务器的结构通常如上图,主要有 I/O 单元、逻辑单元和网络存储单元组成,每个单元之间通过请求队列进行通信,从而协同完成任务。

在项目中,I/O 单元用于处理客户端连接,读写网络数据;逻辑单元用于处理业务逻辑;网络存储单元用于存储本地数据库和文件。

模块单个服务器程序服务器机群
I/O 处理单元处理客户连接,读写网络数据作为接入服务器,实现负载均衡
逻辑单元业务进程或线程逻辑服务器
网络存储单元本地数据库、文件或缓存数据库服务器
请求队列各单元之间的通信方式各服务器之间的永久 TCP 连接

2. 五种 I/O 模型

  • 阻塞 I/O :调用函数后,必须等待调用函数返回后才进行下一步操作;
  • 非阻塞 I/O :调用函数后,每隔一段时间检测 I/O 事件是否就绪,若没有就绪则可以做其他事情;若就绪则继续执行后续代码。非阻塞 I/O 执行系统调用总是立即返回,而不管 I/O 事件是否就绪,需要根据 errno 区分具 I/O 事件是否就绪;
  • 信号驱动 I/O :调用函数后,不再主动检测 I/O 事件是否就绪,而是等待事件完成或发生错误后进程收到的信号来判断 I/O 事件是否就绪,因此该方法是非阻塞的;
  • I/O 复用:主要指 Linux 使用的 3 个函数,它们均能同时监听多个事件:
    • select :会使进程阻塞,没有将文件描述符和事件进行绑定,它仅仅是一个文件描述符的集合,因此使用时需要遍历所有事件,来查找目标事件;
    • poll :会使进程阻塞,将文件描述符和事件绑定,所有事件都被统一处理,使用时同样需要遍历所有事件,来查找目标事件;
    • epoll :在内核中维护一个事件表,提供 epoll_ctl 方法来控制其中的事件(增、删、改),使用时只需要遍历就绪事件(前 n 个)。
  • 异步 I/O :Linux 可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓存区后在通知应用程序。

前四种 I/O 模型都是同步的,即内核向应用程序通知的是就绪事件,需要用户代码对其执行操作;异步 I/O 则是指内核像应用程序通知完成事件,即内核已完成 I/O 操作。

3. 事件处理模式

  • reactor :主线程(I/O 处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元),所有 I/O 具体的操作均在工作线程中完成,通常为同步的模型;
  • proactor :主线程和内核负责处理 I/O 的具体操作(如读写数据),工作线程仅负责业务逻辑(如处理客户请求),通常为异步的。由于异步 I/O 模型并不成熟,因此项目中使用同步 I/O 模拟实现 proactor 模式。

同步 I/O 模型的工作流程如下:

  • 主线程往 epoll 内核事件表注册 socket 上的读就绪事件;
  • 主线程调用 epoll_wait 等待 socket 上有数据可读;
  • 当 socket 上有数据可读,epoll_wait 通知主线程,此时主线程从 socket 循环读取数据,读完后将数据封装成一个请求对象插入请求队列;
  • 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理,然后往 epoll 内核事件表中注册 socket 上的写就绪事件;
  • 主线程调用 epoll_wait 等待 socket 可写;
  • 当 socket 上有数据可写,epoll_wait 通知主线程,此时主线程往 socket 上写入服务器处理客户请求的结果。

4. 半同步/半反应堆模式

项目中使用的并发模式指 I/O 处理单元与逻辑单元的协同方式,该模型是半同步/半异步模型的变体,同步指的是程序完全按照代码顺序执行,这是一个串行的方式;异步指的是程序的执行需要由系统事件驱动,也就是说多个事件(多段代码)同时进行。

半同步/半异步模式的工作流程如下:

  • 异步线程用于处理 I/O 事件;
  • 异步线程监听到客户请求后,就将其封装成请求对象并插入到请求队列中;
  • 请求队列将通知某个工作线程(同步模式的线程,用于处理客户逻辑)来读取并处理该请求。

半同步/半反应堆模式的工作流程如下(proactor 模式):

  • 主线程充当异步线程,负责监听所有 socket 上的事件;
  • 若有新请求到来,主线程接收并得到新连接的 socket ,然后往 epoll 内核事件表中注册该 socket 上的读写事件;
  • 如果该 socket 上有读写事件发生,主线程从 socket 上接收数据,并将数据封装成请求对象插入到请求队列中;
  • 所有工作线程(同步模式的线程)睡眠在请求队列行,当有任务到来时,通过竞争获得任务的接管权。

5. 线程池

一般的任务中,当需要使用线程处理工作时,我们才构造一个线程出来,而当任务完成时则回收该线程防止占用资源。但是这样的方式在服务器中会造成很大的时间开销,因此可以采用线程池的方式用空间换时间:

  • 池表示一组资源的集合,这组资源在服务器启动之初就被创建并初始化,称为静态资源;
  • 在服务器运行阶段,如有任务需要使用线程,则可以直接从线程池中获取,无需动态分配;
  • 当一个线程工作完成后,无需将其销毁,只需要放入池中即可。

二、代码解析

1. 线程池类定义

template <typename T>
class threadpool
{
public:
    // 构造函数
    // actor_model:工作模式
    // connPool:数据库连接池指针
    // thread_number:线程池中线程的数量
    // max_request:请求队列中最多允许的、等待处理的请求的数量
    threadpool(int actor_model, connection_pool *connPool, int thread_number = 8, int max_request = 10000);
    
    // 析构函数
    ~threadpool();
    
    // 将http请求插入请求队列
    bool append(T *request, int state);
    bool append_p(T *request);

private:
    // 工作线程运行的函数,它不断从工作队列中取出任务并执行之
    static void *worker(void *arg);

    void run();

private:
    int m_thread_number;         // 线程池中的线程数
    int m_max_requests;          // 请求队列中允许的最大请求数
    pthread_t *m_threads;        // 描述线程池的数组,其大小为m_thread_number
    std::list<T *> m_workqueue;  // 请求队列
    locker m_queuelocker;        // 保护请求队列的互斥锁
    sem m_queuestat;             // 是否有任务需要处理
    connection_pool *m_connPool; // 数据库连接池
    int m_actor_model;           // 模型切换
};

2. 线程池创建与回收

// 构造函数
template <typename T>
threadpool<T>::threadpool(int actor_model, connection_pool *connPool, int thread_number, int max_requests) : m_actor_model(actor_model), m_thread_number(thread_number), m_max_requests(max_requests), m_threads(NULL), m_connPool(connPool)
{
    // 线程数和允许的最大请求数均小等于0,出错
    if (thread_number <= 0 || max_requests <= 0)
        throw std::exception();
    
    // 初始化线程,分配id
    m_threads = new pthread_t[m_thread_number];
    if (!m_threads)
        throw std::exception();
    
    for (int i = 0; i < thread_number; ++i)
    {
        // 循环创建线程,1标识符,2线程属性(NULL为默认),3指定线程将运行的函数,4运行的参数
        if (pthread_create(m_threads + i, NULL, worker, this) != 0)
        {
            delete[] m_threads;
            throw std::exception();
        }

        // 将线程进行分离后,不用单独对工作线程进行回收,它在退出时会自行释放资源
        if (pthread_detach(m_threads[i]))
        {
            delete[] m_threads;
            throw std::exception();
        }
    }
}

// 析构函数
template <typename T>
threadpool<T>::~threadpool()
{
    // 释放id空间即可,脱离线程会自行释放资源
    delete[] m_threads;
}

3. 向请求队列中添加任务

// 向请求队列中添加任务
template <typename T>
bool threadpool<T>::append(T *request, int state)
{
    // 互斥锁加锁
    m_queuelocker.lock();

    // 判断请求队列是否大于最大请求个数
    if (m_workqueue.size() >= m_max_requests)
    {
        m_queuelocker.unlock();
        return false;
    }

    // 设置HTTP请求状态
    request->m_state = state;

    // 向工作队列中添加任务
    m_workqueue.push_back(request);

    // 互斥锁解锁
    m_queuelocker.unlock();
    
    // 通过信号量提示有任务要处理
    m_queuestat.post();
    return true;
}

4. 线程处理函数

// 线程处理函数
template <typename T>
void *threadpool<T>::worker(void *arg)
{
    // 将参数强制转为线程池类,调用成员方法
    threadpool *pool = (threadpool *)arg;

    // 内部访问私有成员函数run,完成线程处理
    pool->run();
    return pool;
}

5. run 执行任务

// 处理HTTP请求
template <typename T>
void threadpool<T>::run()
{
    // 工作线程从请求队列中取出某个任务进行处理
    while (true)
    {
        // P操作,取出一个任务,先处理信号量能保证线程一定能取到任务,虽然任务的顺序可能会变,但不影响
        m_queuestat.wait();

        // 互斥锁加锁
        m_queuelocker.lock();

        // 请求队列为空则继续循环
        // 个人理解不需要该步骤,因为如果执行了,相当于取了一个工作线程信号量,但是没有取工作线程,出现bug
        if (m_workqueue.empty())
        {
            m_queuelocker.unlock();
            continue;
        }

        // 取第一个任务后互斥锁解锁
        T *request = m_workqueue.front();
        m_workqueue.pop_front();
        m_queuelocker.unlock();

        // 空请求,继续循环
        if (!request)
            continue;

        // 模式1表示reactor
        if (1 == m_actor_model)
        {
            // 读请求
            if (0 == request->m_state)
            {
                // 读完缓存区内容或用户关闭连接,1表示缓存区正常读完
                if (request->read_once())
                {
                    // 设置improv
                    request->improv = 1;

                    // 从连接池中取出一个数据库连接
                    connectionRAII mysqlcon(&request->mysql, m_connPool);
                    
                    // 处理http请求的入口
                    request->process();
                }
                // 未读完
                else
                {
                    request->improv = 1;
                    // 计时器标志
                    request->timer_flag = 1;
                }
            }
            else
            {
                // 写请求
                if (request->write())
                {
                    request->improv = 1;
                }
                else
                {
                    request->improv = 1;
                    request->timer_flag = 1;
                }
            }
        }
        // 模式2表示proactor
        else
        {
            connectionRAII mysqlcon(&request->mysql, m_connPool);
            request->process();
        }
    }
}

参考文献

[1] 最新版Web服务器项目详解 - 02 半同步半反应堆线程池(上)
[2] 最新版Web服务器项目详解 - 03 半同步半反应堆线程池(下)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/527511.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件测试 自动化测试selenium篇(一)

目录 一、什么是自动化测试 单元测试 接口自动化 UI自动化 二、如何实施自动化测试 自动化测试需要了解的技能 三、selenium介绍 webdriver的工作原理&#xff1a; 四、SeleniumJava环境搭建 验证环境是否搭建成功 创建java项目&#xff0c;添加pom文件中添加依赖 常见问…

一些使用树莓派遇到的问题

在长达两个月的实习期&#xff0c;白天去公司实习&#xff0c;晚上回到实验室学习树莓派。终于简单的入门了。来做个总结。 目录 1. wifi问题 树莓派之wifi连接 2.vnc树莓派桌面问题 3.安装库或者下载东西时&#xff0c;域名无法解析。 4.进行安装或者操作时&#xff0c…

【Java多线程编程】解决线程的不安全问题之volatile关键字

目录 1. 造成线程不安全的代码 2. volatile能保证内存可见性 3. synchronized与volatile的区别 3.1 synchronized能保证原子性 3.2 volatile不能保证原子性 1. 造成线程不安全的代码 有一代码&#xff0c;要求两个线程运行。并自定义一个标志位 flag&#xff0c;当线程2&…

UVM学习——搭建简单的UVM平台

引言 本专栏的博客均与 UVM 的学习相关&#xff0c;学习参考&#xff1a; 【1】UVM Tutorial 【2】张强著&#xff0c;UVM实战 &#xff08;卷 Ⅰ&#xff09; 【3】Download UVM (Standard Universal Verification Methodology) 本专栏的学习基本依照 资料【2】的主线&#…

【机器学习】正规方程法求解线性回归问题

前情提要&#xff1a;https://blog.csdn.net/weixin_45434953/article/details/130604086 正规方程 正规方程能以更好的方式求得假设函数中 θ \theta θ的最优值。它提供了一种用于求 θ \theta θ的解析方法&#xff0c;而不是梯度下降那样的迭代方法。也就是只需要一次运算…

Microsoft Power BI连接本地mysql 数据库 !power bi提示此连接器需要安装一个或多个其他组件才能使用怎么办!

一、步骤 &#xff08;一&#xff09;从菜单栏点击进入mysql数据库 点击主页>获取数据>更多 选择mysql数据库&#xff0c;点击连接 &#xff08;二&#xff09;已经安装了mysql connector/net还是提示此连接器需要安装一个或多个其他组件才能使用-解决 重装了几次都…

vue-cli 关闭 Uncaught error 的全屏提示

在使用vue-cli开发项目的时候&#xff0c;如果代码抛出异常了&#xff0c;那么就会出现一个全屏的提示框&#xff0c;长下面这样&#xff1a; 经过一段时间的排查发现是webpack的问题&#xff0c;排查方式就是打开控制台&#xff0c;看这个框的一些属性&#xff0c;通常会有一些…

【DNDC模型】在土地利用变化、未来气候变化下的建模方法及温室气体时空动态模拟实践技术

DNDC&#xff08;Denitrification-Decomposition&#xff0c;反硝化-分解模型&#xff09;是目前国际上最为成功的模拟生物地球化学循环的模型之一&#xff0c;自开发以来&#xff0c;经过不断完善和改进&#xff0c;从模拟简单的农田生态系统发展成为可以模拟几乎所有陆地生态…

界面开发框架Qt新手入门教程 - 可编辑树模型的示例(一)

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff0c;Qt已被运用于超过70个行业、数千家企业&#xff0c;支持数百万设备及应用。 The Qt Company是Di…

考研日语-详解ている、てある、ていく、てくる用法

目录 一、ている用法 1. 表示现在状态 2. 表示持续动作 3. 表示经验或习惯 4. 表示结果或效果 二、てある用法 1. 表示已经完成的动作 2. 表示现在状态 3. 表示被动 三、ていく用法 1. 表示未来的动作 2. 表示逐渐变化的过程 四、てくる用法 1. 表示过去到现在的…

Python实现哈里斯鹰优化算法(HHO)优化XGBoost分类模型(XGBClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 2019年Heidari等人提出哈里斯鹰优化算法(Harris Hawk Optimization, HHO)&#xff0c;该算法有较强的全…

滴滴一面:BigKey问题很致命,如何排查和处理?

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易、滴滴的面试资格&#xff0c;遇到一几个很重要的面试题&#xff1a; 致命的的Redis BigKey 如何排查&#xff0c;你处理过吗&#xff1f; 与之类似…

Yield Guild Games: RON 质押来啦!

Yield Guild Games (YGG) 自 2022 年 7 月以来一直是 Ronin 区块链的验证者&#xff0c;在保障和维护网络方面发挥着至关重要的作用。随着 2023 年 4 月委托权益证明 (DPoS) 的推出&#xff0c;质押生态系统进一步民主化&#xff0c;允许更多的参与者在赚取奖励的同时为网络的安…

LabVIEWCompactRIO 开发指南19 原始以太网(TCP/UDP)

LabVIEWCompactRIO 开发指南19 原始以太网&#xff08;TCP/UDP&#xff09; TCP和UDP是所有以太网标准的低级构建块。原始TCP和UDP工具在几乎所有编程环境中都得到原生支持&#xff0c;包括LabVIEW。它们提供较低级别的通信功能&#xff0c;这些功能更灵活&#xff0c;但用户…

3.34 haas506 2.0开发教程-example -TFT显示自动生成的二维码

TFT显示自动生成的二维码 应用场景案例说明1.硬件2.连线图 代码源码链接[TFT显示二维码](https://www.yuque.com/haas506/wiki/pubazmzgrf30zws0) 应用场景 二维码在各个领域中的应用越来越广泛&#xff0c;其中一些主要应用场景包括&#xff1a; 电子商务&#xff1a;通过二维…

RHCSA之查看命令帮助手册

目录 RHCSA之查看命令帮助手册 查看命令类型 --- type Linux中对应的命令类型 帮助命令 help 命令 用法1 help 内部命令 用法2 命令 --help 命令的部分语法符号解析 man 命令 man命令用法 man的帮助级 man 命令帮助信息界面中的常用操作 man命令中帮助信息的结构以及意义…

CGAN(条件GAN)

相比于GAN&#xff0c;CGAN给生成器和辨别器都添加了一个辅助信息&#xff0c;假设为y,y可以是标签类别或者其他模态的信息。 目标函数相比于GAN在输入端的x和z变为在y条件下生成的x和z。 模型框架可以表示为&#xff1a; 代码&#xff1a; import argparse import os os.en…

如何在linux中配置JDK环境变量

在linux系统部署皕杰报表&#xff0c;因皕杰报表是一款纯java报表工具&#xff0c;运行时需要jre环境&#xff0c;所以要在服务器上配置三个jdk环境变量path、classpath、JAVA_HOME。 那么为什么要配置jdk环境变量呢&#xff1f;因为java软件运行时要用到一些java命令&#xff…

人体样本? 一站式医学微生态研究解决方案来啦!

细菌&#xff1f;真菌&#xff1f;古菌&#xff1f;病毒&#xff1f; 还在为人体微生态研究选择哪个切入点而苦苦纠结吗&#xff1f; 数据&#xff1f;算法&#xff1f;作图&#xff1f;分析&#xff1f; 还在苦于已有的分析内容脱离医学临床实际而不知所措吗&#xff1f; …

vue 3.0 静态路由配置

今天研究了一下vue3.0的静态路由配置&#xff0c;分享一下。 首先我们现在项目中建立router文件夹&#xff0c;如下图所示&#xff1a; 404文件夹存放404页面&#xff0c;components文件夹存放首页界面&#xff0c;config为设置文件夹&#xff0c;diz存放具体的业务逻辑和界面…