news 2026/6/19 6:19:37

Linux多线程编程(五):线程池实现与线程安全的单例模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux多线程编程(五):线程池实现与线程安全的单例模式

Linux 多线程编程(五):线程池与线程安全的单例模式

高效管理线程资源,掌握并发编程中的两大实用设计模式


前言

在前几篇文章中,我们分别讨论了条件变量、信号量以及它们在生产消费者模型中的应用。这些机制解决了线程间的同步与通信问题,但当我们面临大量短任务突发高并发场景时,频繁创建和销毁线程本身就会成为系统瓶颈。此时,线程池(Thread Pool)应运而生。

同时,在服务器开发中,许多全局资源(如配置信息、缓存数据)只需要一个实例,单例模式(Singleton)成为标配。然而,单例模式在多线程环境下必须小心设计,否则可能导致资源重复创建或数据不一致。

本文将系统讲解线程池的原理与实现,并深入探讨线程安全的单例模式,包括经典的“双重检查锁定”和 C++11 的现代写法。


一、线程池(Thread Pool)

1.1 为什么需要线程池

线程的创建和销毁是有开销的——每次创建都需要分配内核资源、建立堆栈、设置 TLS;销毁则要回收这些资源。如果一个任务执行时间非常短(如处理一个 HTTP 请求),那么创建线程的时间可能远超任务本身的时间,得不偿失。

线程池通过预先创建固定数量的线程,让它们循环从任务队列中取任务执行,从而:

  • 减少线程创建/销毁开销:线程复用,只创建一次。
  • 控制并发数量:防止线程数过多导致 CPU 频繁切换、内存耗尽。
  • 提升响应速度:任务到达时无需等待线程创建,立即分配空闲线程执行。

1.2 适用场景

  • Web 服务器:每个请求是一个短任务,需要快速响应。
  • 批量数据处理:将大任务拆分为多个小任务并行处理。
  • 突发流量:如秒杀系统,瞬间大量请求,线程池可平滑消化。

对于长时间运行的任务(如 Telnet 长连接),线程池收益不大,建议单独创建线程。

1.3 线程池的基本组件

一个典型的线程池包含:

  • 任务队列(Task Queue):存放待执行的任务对象。
  • 工作线程(Worker Threads):固定数量的线程,循环从队列中取任务并执行。
  • 同步机制:互斥锁保护队列,条件变量用于线程等待/通知。
  • 管理接口:提交任务、停止线程池等。

1.4 C 风格线程池实现解析

下面是课件中提供的线程池实现,我们逐段分析其设计思路。

(1)任务抽象类ThreadTask
typedefbool(*handler_t)(int);classThreadTask{private:int_data;handler_t _handler;public:ThreadTask(intdata,handler_t handler):_data(data),_handler(handler){}voidRun(){_handler(_data);}};

每个任务封装了一个整型数据和一个函数指针,Run()负责执行具体的处理逻辑。这种设计将数据操作解耦,便于扩展。

(2)线程池类ThreadPool

成员变量

  • _thread_max:线程池容量(固定)。
  • _thread_cur:当前存活的工作线程数(用于安全退出)。
  • _tp_quit:退出标志。
  • _task_queue:任务队列(存放ThreadTask*)。
  • _lock/_cond:互斥锁和条件变量。

工作线程入口函数thr_start

staticvoid*thr_start(void*arg){ThreadPool*tp=(ThreadPool*)arg;while(1){tp->LockQueue();while(tp->IsEmpty()){tp->ThreadWait();// 条件等待,内部处理退出逻辑}ThreadTask*tt;tp->PopTask(&tt);tp->UnLockQueue();tt->Run();deletett;}returnNULL;}

关键点:

  • 使用while检查队列空,防止虚假唤醒。
  • _tp_quittrue时,ThreadWait()会调用ThreadQuit()减少当前线程计数并退出线程(调用pthread_exit)。
  • 取出任务后先解锁再执行,避免在任务执行期间阻塞其他线程提交任务或取任务。

任务提交PushTask

boolPushTask(ThreadTask*tt){LockQueue();if(_tp_quit){UnLockQueue();returnfalse;}_task_queue.push(tt);WakeUpOne();// 唤醒一个等待线程UnLockQueue();returntrue;}
  • 先加锁,检查退出标志,若线程池正在退出则拒绝新任务。
  • 入队后调用pthread_cond_signal唤醒一个工作线程(避免惊群)。

停止线程池PoolQuit

boolPoolQuit(){LockQueue();_tp_quit=true;UnLockQueue();while(_thread_cur>0){WakeUpAll();// 唤醒所有线程,让它们检查退出标志并退出usleep(1000);}returntrue;}
  • 设置退出标志,然后广播唤醒所有线程。
  • 循环等待直到所有线程都退出(_thread_cur归零)。

补充:这里使用了usleep轮询,更优雅的方式是使用pthread_cond_timedwait或引入同步计数器。

(3)主函数测试
boolhandler(intdata){srand(time(NULL));intn=rand()%5;printf("Thread: %p Run Task: %d--sleep %d sec\n",pthread_self(),data,n);sleep(n);returntrue;}intmain(){ThreadPoolpool(5);// 5个工作线程pool.PoolInit();for(inti=0;i<10;i++){ThreadTask*tt=newThreadTask(i,handler);pool.PushTask(tt);}pool.PoolQuit();return0;}
  • 创建 5 个线程,提交 10 个任务,每个任务随机休眠 0~4 秒。
  • PoolQuit会等待所有任务执行完毕(因为线程会在队列空时等待,但设置_tp_quit后,即使队列空也会退出)。

注意:该实现为单生产者-多消费者模型,队列未限制容量,若生产者过快可能堆积大量任务(内存风险)。实际项目中可增加队列上限,或使用阻塞队列(见本系列第三篇)。


二、线程安全的单例模式

2.1 什么是单例模式

单例模式保证一个类只有一个实例,并提供一个全局访问点。常用于:

  • 配置管理类
  • 日志系统
  • 数据库连接池
  • 缓存管理器

2.2 饿汉模式(Eager Initialization)

template<typenameT>classSingleton{staticT data;public:staticT*GetInstance(){return&data;}};// 在全局区定义 static T Singleton<T>::data;
  • 在程序启动时(main 之前)完成实例化。
  • 线程安全:静态初始化在 C++11 前由编译器保证是线程安全的(但标准未强制,实际大部分实现安全;C++11 起保证)。
  • 缺点:如果对象很大且很少使用,会浪费内存,并拖慢程序启动速度。

2.3 懒汉模式(Lazy Initialization)——非线程安全

template<typenameT>classSingleton{staticT*inst;public:staticT*GetInstance(){if(inst==NULL){inst=newT();}returninst;}};
  • 第一次调用GetInstance时才创建对象。
  • 严重问题:多线程下可能同时进入if条件,创建多个实例,违背单例意图。

2.4 线程安全的懒汉实现:双重检查锁定(Double-Checked Locking)

#include<mutex>template<typenameT>classSingleton{volatilestaticT*inst;// 防止编译器优化staticstd::mutex lock;public:staticT*GetInstance(){if(inst==NULL){// 第一次检查,避免每次调用都加锁lock.lock();if(inst==NULL){// 第二次检查,确保单例inst=newT();}lock.unlock();}returninst;}};

核心要点

  • 双重判断:外层if避免无谓的锁竞争,内层if确保只有一个线程进入new
  • volatile关键字:防止编译优化导致指令重排。例如,new T()可能被分解为分配内存、构造对象、赋值给inst,若重排后inst先指向未构造完毕的内存,其他线程可能拿到半成品对象。volatile告知编译器不要重排与inst相关的指令(但volatile在 C++ 中不能完全解决内存序问题,更推荐使用原子操作)。
  • C++11 及以后:可使用std::call_onceMagic Static(见下文)。

2.5 C++11 的 Magic Static(最推荐方式)

C++11 规定:函数内的局部静态变量初始化是线程安全的(编译器会插入保护机制)。

template<typenameT>classSingleton{public:staticT&GetInstance(){staticT instance;// 第一次调用时初始化,线程安全returninstance;}};
  • 简洁、高效、无锁。
  • 适用于 C++11 及以上标准(现代项目首选)。

三、总结

线程池

  • 本质:预创建线程 + 任务队列 + 同步机制。
  • 优势:降低线程创建开销、控制并发数量、提升响应速度。
  • 扩展:可增加任务队列上限、动态调整线程数(如 Java 的ThreadPoolExecutor)、支持定时任务等。

单例模式

  • 饿汉:简单,线程安全,但启动时加载。
  • 懒汉:延时加载,但需处理线程安全。
  • 双重检查锁定:经典写法,注意volatile和内存屏障。
  • C++11 Magic Static:现代最佳实践,代码极简且安全。

线程池和单例模式是并发编程中的基础设施,熟练掌握它们的设计与实现,能让你写出更健壮、更高效的服务端程序。在实际开发中,推荐使用 C++11 的std::thread配合std::function和可变参数模板,使线程池更加灵活易用;单例模式则优先采用局部静态变量方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/19 6:13:00

Qwen3.5-Flash深度实测:T4上工业级低延迟推理全链路解析

1. 项目概述&#xff1a;这不是一次普通模型测速&#xff0c;而是一场面向真实业务场景的“轻量级推理压测”最近在给一个边缘侧智能巡检系统做模型选型&#xff0c;客户明确要求&#xff1a;单卡T4&#xff08;16G显存&#xff09;上跑得动、首token延迟低于300ms、连续生成20…

作者头像 李华
网站建设 2026/6/19 6:03:22

你的下一个知己,何必是碳基生物?----猫娘计划「Project N.E.K.O.」

认识的人那么多&#xff0c;怎么偏偏就和ta成了朋友&#xff1f; 人海里的相遇是随机的&#xff0c;但能留下来一直陪着你的&#xff0c;绝不是凑巧。 朋友似乎总是我们通过时间精挑细选出来的结果&#xff0c;ta总能在你需要ta的时候出现。并且总能以不同的形式来帮助你。 …

作者头像 李华