线程池收尾-回调单例与可重入线程安全

线程池收尾:回调、单例与"可重入 vs 线程安全"(8-22 ~ 8-23)

我的github:(https://github.com/xcx55/ubuntu-linux-project)

感谢各位大佬参观我的github!!

线程池从 8-20 的设计日志起步,这几天收尾:把子线程的执行策略讲透、把"回调"这个设计点破,再用单例模式给线程池定下唯一入口。顺带把线程安全与可重入的关系彻底理清------这两个词以前总混着用,现在是分得清的了。


一、线程池就是一个"带预备队"的生产者消费者模型(8-22)

1. 一句话定性

线程池本身是一个生产者消费者模型!只是多了"提前创建好的一批等待条件变量的线程"这个预备队。

任务队列是"交易场所",主线程/业务线程是生产者,池里的子线程是消费者------只不过消费者是开机就建好、一直在等待队列里睡着的。

2. 子线程 routine 的标准骨架

笔记里写下了子线程的执行结构,逻辑非常清晰:

cpp 复制代码
while (true) {
    pthread_mutex_lock(&mutex, nullptr);
    cond.wait();                      // ① 先睡着,等条件变量唤醒
    if (/* lambda 队列非空 */) {
        task();                       // ② 取出任务,执行 lambda 回调
    }
    // ③ 回锁、处理其他分支
    pthread_mutex_unlock(&mutex);
}

时序一句话概括:先唤醒、执行、最后执行完 lambda 再休眠------创建出来后立刻进入条件变量休眠,任务来了被唤醒,干完活继续睡。

3. 回调函数:底层调用上层

笔记里标了三个重点号:底层调用上层函数(回调)很重要。

这是线程池作为一个"基础设施"的关键设计:线程池在底层 负责调度和执行,但它不知道要执行什么 ------真正的业务逻辑由上层用 lambda/函数对象注册进来,底层反过来调用上层注册的代码。

我们压入任务,不写函数,写 lambda 表达式 的默认类:[捕获] (参数) -> 返回类型 { 函数体 }

编译器为每个 lambda 生成一个匿名类的实例,operator() 里装着函数体------所以"压入任务"压的其实是一个可调用对象 。线程池只管 task(),至于这行代码是什么,是上层的事。


二、线程安全与可重入:终于分清了(8-23)

1. 关系一句话

可重入一定线程安全,线程安全不一定可重入!

  • 可重入:同一函数被"打断后再次进入"也不会出错------这是更强的要求,所以它自然满足线程安全;
  • 线程安全 :多线程并发调用不出错------可以用加锁实现,但加了锁的函数恰恰可能不可重入!

2. 那个致命的死锁场景

笔记里的例子极其经典:

正在执行 as() 函数,而这个信号的信号处理函数 也是 as()------再调一次就会造成死锁!

推演一下:as() 内部加了锁 → 执行到一半来了信号 → 信号 handler 又是 as() → 再申请同一把锁 → 锁已经被自己(同一个执行流)持有 → 永久阻塞。

所以结论是:

除了信号重入这种情况,函数可重入就是线程安全的;线程安全不一定函数可重入------因为信号重入会导致死锁。

这段推理把之前信号章的"自动屏蔽字"和线程章的"互斥锁"缝在了一起:信号 handler 里绝不能调用会加锁的函数------这就是自动屏蔽字只能挡同号信号、挡不住"handler 调用了别处也用的锁"的原因。


三、单例模式:给线程池一个唯一入口(8-23)

1. 动机

线程池是"全局只有一份"的设施:多个模块都要投递任务,如果各自 new 一个线程池,线程数失控、任务队列互相看不见。所以需要单例模式 ------一个类型只能实例化一个对象。

2. 实现要点

cpp 复制代码
class ThreadPool {
private:
    static ThreadPool* _instance;              // 保存自己的地址
    ThreadPool() {}                            // 构造函数私有
    ThreadPool(const ThreadPool&) = delete;    // 拷贝构造 ban 掉
    ThreadPool& operator=(const ThreadPool&) = delete;  // 赋值 ban 掉
public:
    static ThreadPool* GetInstance();          // 唯一入口
};

笔记里的措辞很直白:直接 ban 掉所有构造函数,要 ban 掉拷贝构造、赋值构造、复制构造;ban 掉之后使用唯一入口函数。

程序根本编译不过去------没有构造函数的类,外部连实例都造不出来,这就是"唯一"的强制保障。

3. 两种创建时机:饿汉 vs 懒汉

  • 加载时创建(饿汉):程序启动就 new 好,简单但可能浪费;
  • 运行时创建 (懒汉):第一次 GetInstance() 才创建------这才叫单例(按需),工业界更常用(需配双检锁保证线程安全)。

4. 单例模式的价值

单例,就是用一个类的实例 ,一直复用它的 GetInstance()。

三个实际好处:

  1. 避免重复初始化------有些类的构造带 IO 操作,反复"初始化→销毁"代价极高;
  2. 免去函数间传参 ------main 里初始化好后,任何地方调 GetInstance() 就能拿到,比一层层传指针干净得多;
  3. 全局唯一语义------线程池、日志器、配置中心天生就该只有一份。

四、小结

主题 结论
线程池本质 生产者消费者模型 + 预热好的消费者线程
子线程 routine while(true) → lock → cond.wait → 判队列 → task() → unlock
回调 底层框架调用上层注册的 lambda(匿名类 + operator())
可重入 vs 线程安全 可重入 ⇒ 线程安全;线程安全 ⇏ 可重入(信号重入 handler 会死锁)
单例 ban 构造/拷贝/赋值 + 静态指针 + 唯一入口 GetInstance
单例价值 避免重复 IO 初始化、免传参、全局唯一

到这里,线程章从"轻量级进程"一路走到"线程池 + 单例",工程化的闭环算是合上了。

相关推荐
牢姐与蒯2 小时前
Linux信号(二).信号产生续 && 信号保存
linux·运维·服务器·ubuntu
SoStraw2 小时前
跨平台P2P通信SDK,Windows/Android/iOS/Linux/macOS一套API全兼容
android·linux·windows·跨平台·p2p·webrpc
皓月盈江2 小时前
WordPress博客迁移到Hexo静态博客教程
linux·服务器·hexo·wordpress·博客迁移·静态博客
中杯可乐多加冰2 小时前
解决方案:CEB 文件 Web 端在线预览方案,Linux部署ceb文件在线预览工具
linux·运维·前端·ceb
一条破秋裤2 小时前
Linux 进程通信介绍:内存隔离与 IPC 分类
java·linux·服务器
码完就睡2 小时前
Linux——计算机网络基础概论
linux·计算机网络
别动我齐刘海2 小时前
ROS2 Jazzy + C++ 时间系统——Time / Clock / Duration
linux·c++·人工智能·学习·机器学习·机器人·自动驾驶
cuijiecheng20182 小时前
MobaXterm上传大文件到rk3588板子失败
linux
半仙白桑2 小时前
内核篇第十三讲:system-V消息队列
java·linux·网络