手把手教你实现一个完美的线程池(可商用)第一集 c++高阶开发

众所周知 频繁的创建和销毁线程所带来的开销是很大的 一个高性能高并发得服务器都会需要用到线程池技术 那么我们怎么设计一个完美的线程池呢?

1. 我们需要一个线程类,具体的线程函数应该是实际需要的时候 再设置。

2. 不应该限制线程函数的参数数量、作用域(可以是全局函数、 静态成员函数、类成员函数等等)。

3. 我们规定线程函数返回值应该是int,0表示任务结束 1表示需 要再次运行任务 。

4. 线程应该可以控制,在需要的时候启动,并且随时可以终止和 退出。

5. 这个类不应该具有赋值和复制构造方法,因为线程本身不具有 可复制性!

如果读者不同意以上五个观点 那么就没有必要读下去的必要

考虑到第二点,我们必然需要泛型编程技术,或者叫模板编程技术。 基于这些需求我们大概有一个初步的构想。

类名CThread

考虑到很多地方都这么给线程类起名,所以我们可以加入一个名称 空间GodKillerLi。 接口应该有下面这些:

1. 启动/恢复

2. 暂停

3. 停止

4. 重新开始线程

5. 线程是否有效

线程API我们选取Windows下的CreateThread (直接选择系统调用 不要搞些奇奇怪怪的东西浪费效率 如果是linux 则选择pthread_create ) 这个API函数说明如下:(参考自windows官方文档)

#define WINAPI     __stdcall
typedef void *HANDLE;
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter
   );
typedef PTHREAD_START_ROUTINE
LPTHREAD_START_ROUTINE;
HANDLE
WINAPI
CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES
lpThreadAttributes,//线程属性
    _In_ SIZE_T dwStackSize,//线程栈的大小,为0表示
默认大小
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,//
线程入口函数
    _In_opt_ __drv_aliasesMem LPVOID
lpParameter,//线程参数
    _In_ DWORD dwCreationFlags,//创建标志默认写0,
CREATE_SUSPENDED线程暂停,
STACK_SIZE_PARAM_IS_A_RESERVATION 第二个参数有效(默
认忽略)
    _Out_opt_ LPDWORD lpThreadId //线程ID
   );

从上面我们知道,线程的API接口需要我们填入一个__stdcall调用约 定的函数,做为线程入口函数。 而且函数参数是固定的,和前面的需求2冲突。 所以我们必然需要一个过度量来记录用户提供的自定义线程函数, 另外需要额外提供一个入口函数,来做为系统API函数的参数。 考虑到线程后面可能用于线程池,所以线程类本身不能是模板类。

注意:模板参数一旦不同,系统会认为这些类是不同的类。所以 用于线程池的时候,会比较麻烦——我们不太可能给每一个子线 程提供控制接口。因为线程池要求所有子线程最好都是同一个类 型,这样扩容或者收缩的时候,就容易实现动态分配和释放。

所以我们还需要一个模板函数类,来接入这些用户动态设置的自定 义线程函数。 由于这个函数类是模板类,所以我们还需要给它设置一个基类,方便线程类来持有它。 避免模板参数的需求传递到线程类来了。 

注意:函数类也叫仿函数,一般需要重构函数调用运算符 operator()(...)。

首先我们要把仿函数(函数类)搞出来,否则线程类没法实现。 仿函数代码如下:

namespace GodKillerLi {
 class CFunctionBase
 {
 public:
 virtual ~CFunctionBase() = default;
 virtual int operator()() = 0;
 };
 template <class _FX, class... _Types>
 class CFunction :public CFunctionBase
 {
 public:
 CFunction(_FX _Func, _Types... _Args)
 :m_binder(std::forward<_FX>(_Func),std::forward<_Types>(_Args)...)
 {}
 virtual ~CFunction() {}
 virtual int operator()() {
 return m_binder();
 }
std::_Binder<int, _FX, _Types...>m_binder;
 };
}

 仿函数CFunction必须有一个基类CFunctionBase。 这样线程类里面的具体函数可以通过基类指针,来持有函数类。 仿函数最重要的接口就是运算符重载函数。 所以这里我们要虚函数的特性,将函数调用运算符重载函数,设置 为虚函数。 并且要设置为纯虚函数,这样之类如果不实现该接口,则无法实例 化,无法声明对象。 考虑到到时候我们都是通过基类指针操作,为了子类的析构,所以 这里我们要设置虚析构,避免析构的时候出现意外。 基类基本就是一个接口类,所以完全可以不写构造函数。

template 这是一段模板参数声 明代码。 告诉编译系统这样一个事实:下面有一个类型变量FX,和一个可变 参数模板,我们叫它们Types。 至于FX和Types怎么用,需要我们这些开发者来写代码告诉编译系统。

CFunction(_FX _Func, _Types... _Args)
 :m_binder(std::forward<_FX>(_Func),std::forward<_Types>(_Args)...)

 这段代码则首先指明这样一个事实: 构造函数有一个可变类型的参数_Func,和若干各可变参数类型 _Types... 我们的形式参数Func的类型是FX,_Args则是指代所有可变参数——这绝不是一个参数,但也可能一个参数也没有。 声明清楚这些参数以后,我们很快就需要在m_binder里面使用这些参数。

m_binder是这样一种混合体:包含函数类型和调用参数的混合体。 这种东西很有意思,我们一般写一个函数调用,例如下面这样: func(10,"hello"); 往往执行到这句话的时候,就是func函数被调用的时候。 m_binder则提供了另外一种可能。 写这句话的时候,和执行的时候,被分开了。 在绑定的时候——也就是binder的英文本意,代码并没有被执行。 而什么时候执行,则完全由用户说了算。 唯有执行函数调用运算符的时候,才是真正执行代码的时候。 这种延迟执行的特性,非常重要! 线程函数执行的时候,往往和声明的时候,不是在同一个时间。 有了这个工具,我们就可以准备一个m_binder,去承接用户多样化 的线程函数和函数参数了。 等到需要执行的时候,我们再真正执行这些函数。 这里还有一个小知识,std::forward表示完美转发。

关于左值右值 左值引用 右值引用 万能引用 引用折叠 完美转发等知识点可以阅读如下文章 谈谈C++的左值右值,左右引用,移动语意及完美转发 - 知乎 

 析构函数里面我们无需做什么事情,因为m_binder会自行进行析 构。 运算符重载我们也无需做什么特别的事情,只需要按照之前的设 置,进行函数调用即可。 这里无需指定参数,因为这些参数已经在构造的时候已经设置好了。

std::_Binder<std::_Unforced, _FX, _Types...> m_binder;

最后就是这个m_binder的声明。我们可以看到_FX,这是函数的类型。 _Types...这个是可变参数的类型。 std::_Unforced是函数的返回类型,意思是可以为void。 也就是无返回类型的函数,也是可以在此设置的。 整个变量就是一个类型不定的函数执行器。 到这里为止,可变的函数类型我们就完成了,下面我们开始线程的实现。

namespace GodKillerLi {
 enum CThreadStatus
 {
 THREAD_AGAIN = 1, //线程再次运行
 THREAD_OK = 0, //线程目前一切
正常
 THREAD_IS_INVALID = -1, //线程无效
 THREAD_PAUSE_ERROR = -2, //线程暂停错误
 THREAD_IS_BUSY = -3, //线程占线,被
强制结束
 };
 class CThread
 {
 public://std::bind
 template< typename _FX, typename...
_Types >
 CThread(_FX _Func, _Types... _Args);
 CThread();//线程池需要
 ~CThread();
 //禁止复制!!!
 CThread(const CThread&) = delete;
CThread& operator=(const CThread&) =delete;
 int Start();//开始/恢复
 int Pause();//暂停
 int Stop();//停止
 int Restart();//重启线程(上一次运行必须结束)
 inline bool isValid() const;//线程是否有效
 template< typename _FX, typename...
_Types >
 int SetThreadFunc(_FX _Func, _Types...
_Args);
 private:
 static DWORD WINAPI ThreadEntry(LPVOID
lpParam);
 void EnterThread();//__thiscall
 private:
 DWORD m_nThreadID;
 HANDLE m_hThread;
 bool m_bValid;//True表示线程正常 False表示线
程不可用
 std::atomic<CFunctionBase*> m_pFunction;
 };
}

头文件如上面代码所示。 首先这个类提供两种构造函数

template< typename _FX, typename... _Types >
CThread(_FX _Func, _Types... _Args);
CThread();

一种是带线程函数设定的构造函数,用于单个线程的使用。一种是 无参数的构造函数,用于线程池的初始化。 线程池往往会批量产生若干个线程对象,这个时候无参数的构造函 数,可以提供方便。否则每个线程都需要进行设置。单个线程使用,则没有这种顾虑,所以保留一个带线程函数设置的 构造函数,可以方便单个的时候使用。

注意,构造函数是模板函数,并不意味着线程类是模板函数! 所以这只是表明我们有一个系列的构造函数,用于设置各种不同 需要执行的线程函数。 并不说明线程类本身变成了模板函数。 一个非模板类可以拥有一个或者多个模板函数。 类前面拥有template声明,则该类是模板类,否则则属于非模板 类。 一个类里面包含纯虚函数,则该类是抽象类,否则则属于普通 类。 抽象类、模板类因素相互不干扰,一个类既可以是模板类,也可 以是抽象类,或者两者都不是。

 下一个我们要注意的是这段代码:

CThread(const CThread&) = delete;
CThread& operator=(const CThread&) =delete;

这段代码表示线程类的复制构造函数和等于号运算符重载被删除。 一旦有涉及复制构造的行为,编译器会及时报错,避免代码进入后 续的执行环节。 进而避免了线程类对象之间的相互复制和传值。

线程在Windows环境下是无法被复制的(当然,在Linux环境也 无法完美复制。) 线程本身是内核对象,我们通过API去使用这个内核对象。 要复制,需要内核提供这个功能——但是目前微软尚未提供该功 能。 不过我们可以复制线程的句柄。复制线程的句柄又会引发一个新 的问题:谁来释放这个句柄? 如果一个线程对象创建了一个线程,另外一个对象复制了它的句 柄。 另外一个对象析构的时候怎么处理? 如果释放了句柄,原创建者再次释放,就可能导致问题。 创建者没有释放,问题更大——在一个无效的线程句柄上进行操 作,导致的可能就不是问题,而是崩溃了。 最好的方案就是每个线程都是独立,不可复制的,独立管理自己 的线程句柄。

接下来就是我们前面讲的线程接口代码: 

int Start();//开始/恢复
 int Pause();//暂停
 int Stop();//停止
 int Restart();//重启线程(上一次运行必须结束)
 bool isValid() const;//线程是否有效
 //设置线程函数
 template< typename _FX, typename... _Types >
 int SetThreadFunc(_FX _Func, _Types... _Args);

 Start表示开启线程,如果线程处于暂停状态,则线程会恢复运行。 Pause表示暂停线程,注意该操作有风险。一旦暂停,线程所属代 码会暂停执行。这意味着对应的文件读写、网络/管道/信号通信、 异步操作等等,都会发生不可逆转的错误。包括但不限于文件卡 死、网络中断、管道关闭、信号丢失等等。 该功能最佳使用场景是线程池进行规模缩减的时候,将空线程进行 暂停,来缩减线程池规模。这样,线程池需要再次扩张的时候,可以直接通过恢复而不是创建来启动线程。 由于是空闲线程,即使持有者决定彻底析构线程时,也不会有太过 分的负面影响。 Stop表示停止线程。如果线程被用户指定的函数卡死而没有反应, 那么会强制结束线程。所以用户指定的线程函数需要遵循一些规 范,不应该处于卡死状态。 Restart表示重启线程函数。当前一个线程函数执行完成之后,该功 能才能起效。否则该功能只会返回错误值。 isValid表示线程有效状态。返回true表示线程状态正常,否则表示 线程无法执行,处于失效状态。用户指定的线程函数可以通过此接 口来决定自己的状态。一旦线程失效,应该尽快退出函数执行,否 则可能随时被强制结束,而引起更加糟糕的情况出现。 SetThreadFunc设置线程函数。注意如果已经设置过线程函数,此 接口仍然可以调用并设置。设置后,下一次执行线程函数,才会生 效。当前执行的函数不受影响。但是,如果线程没有启动,则设置 后,最新的设置起效,旧有的设置会被丢弃。 对外接口明确下来之后,我们就可以开始考虑内部函数了。 

敬请期待下一集咯

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用 Spring Boot 3 开发一个后端分离的生产级系统需要以下步骤: 第一步:环境准备 1. 安装 Java 开发工具包(JDK) 2. 安装集成开发环境(IDE),如Eclipse或IntelliJ IDEA 3. 安装Maven构建工具 4. 安装数据库(如MySQL)和相关工具(如MySQL Workbench) 第二步:创建后端项目 1. 使用IDE创建一个新的Spring Boot项目 2. 配置项目的基本信息,如项目名称、包名等 3. 添加必要的依赖,如Spring Boot Starter Web、Spring Data JPA等 4. 定义实体类、控制器、服务等后端代码 第三步:创建前端项目 1. 使用前端开发工具,如Vue.js或React.js,创建一个新的前端项目 2. 配置项目的基本信息,如项目名称、包名等 3. 定义前端路由、页面、组件等前端代码 第四步:前后端集成 1. 在后端项目中配置跨域访问,允许前端项目访问后端接口 2. 在前端项目中调用后端接口,实现数据的交互 第五步:开发和测试 1. 根据需求逐步开发后端和前端功能模块 2. 使用测试框架,如JUnit和Selenium,对系统进行单元测试和端到端测试 第六步:部署和上线 1. 打包后端项目为可执行的JAR文件 2. 部署JAR文件到生产环境的服务器上 3. 配置服务器的环境变量、数据库连接等 4. 启动服务器,验证系统是否正常运行 通过以上步骤,我们可以完成一个使用Spring Boot 3开发的前后端分离的生产级系统。这种架构可以提高开发效率、降低系统耦合性,并且适合大型项目的开发和部署。同时,我们还可以根据实际需求,进一步优化系统性能、可维护性和安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杀神李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值