Linux之线程池(三)

线程池 + 单例模式

为什么线程池要做单例

线程池是管理一堆工作线程的资源,一个进程只需要一个线程池实例 。 如果到处new ThreadPool,会创建多组线程,抢占 CPU 资源,线程爆炸、资源浪费。 所以工程上经常把线程池做成饿汉 / 懒汉单例,全局唯一,整个程序共用这一组工作线程。

本质就是让某一个类对象在内存中只存在一份

1.将磁盘加载到内存,创建对象

int val=100;

2.进程在运行期间,创建对象

int *val=(int *)malloc(sizeof(int));

本质就是让某一个类对象在内存中只存在一份(在运行或者加载期间)

那么如何做到呢???

有懒汉模式和饿汉模式

吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭. 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.

首先将特定的类的构造函数,拷贝构造,赋值语句全部时private,甚至拷贝构造,赋值语句

饿汉方式实现单例模式--加载的时候形成对象

cpp 复制代码
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};

可以优化一下启动速度

懒汉方式实现单例模式--在使用的时候才进程创建

cpp 复制代码
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};

接下来我们来修改一下我们线程池的代码

懒汉模式

赋值和拷贝构造都要禁止

那么我们应该要怎么形成类对象呢??

将上面构造变成私有,让外部调用函数来获取

  1. 构造私有,外部无法直接构造对象。
  2. 删除拷贝、赋值,防止复制出第二个实例。
  3. 懒加载:第一次调用Instance()才创建线程池,不用就不创建。
  4. new 完直接Start(),拿到实例就已经启动好线程,main 不需要手动调用Start()

在main函数这里就不能用之前的方法

  1. ThreadPool<Task>模板类 ,指定线程池处理的任务类型是Task
  2. ::Instance()静态成员函数,属于类,不是属于某个对象。

单例模式:静态函数不需要对象,直接类名::函数名()调用,返回 ThreadPool<Task>*(指针)

  1. ->:因为Instance()返回的是对象指针 ,访问成员函数要用箭头->,不能用.
  2. Enqueue(t):线程池的入队接口,把任务放进任务队列。

因为单例模式下:

  • 构造函数被 private 私有化 ,外部不能直接ThreadPool<Task> pool;创建局部对象。
  • 整个程序只能有唯一一个线程池对象 ,所有地方都必须通过Instance()拿到那唯一的实例指针。

重点:Instance()是静态函数,没有对象也可以调用 ,它内部 new 出唯一对象,返回对象的地址(指针)。拿到指针之后,用->调用普通成员Enqueue、Stop、Wait

现在完成了最简单的方法来实现,但是这里依然存在着很多的问题

问题:只有第一次初始化的时候并发会出事_instance不为 nullptr 之后,直接 return,完全安全。

  1. 线程 A:判断 _instance == nullptr → true,进入 if,时间片切走
  2. 线程 B:同样判断 _instance == nullptr → true,也进入 if
  3. 线程 B 执行 new ThreadPool<T>()_instance被赋值第一个对象地址
  4. 切回线程 A,继续执行 new ThreadPool<T>()又 new 第二个对象 ,覆盖_instance

结果:

  • 内存泄漏,第一个 new 出来的对象丢失;
  • 出现两个线程池实例,系统里多组工作线程,业务完全错乱。

后续再调用 Instance,_instance已经不为空,直接 return,不会再触发 new,所以后续调用没问题。

所以现在还需要优化加锁

因为我们在处理完一次之后就不用在加锁也不会多创建对象了

接下来我们就可以采用if判断语句来进行优化

线程安全与可重入函数

很多人容易把线程安全可重入搞混,二者有关系,但不完全等价。

1、什么是线程安全

多个线程同时跑同一个函数、访问同一份共享数据,程序不会乱、不会出现奇怪结果,就叫线程安全。

  • 如果函数里面只用局部变量(栈上,每个线程自己一份),基本不会出问题。
  • 如果操作全局变量 /static 静态变量,又不加锁保护,大概率线程不安全。

线程安全解决的是:多线程并发访问共享资源会不会搞坏数据

2、什么是可重入函数

一个函数还没跑完,又被再次调用进来,这种现象就叫重入。 重入之后结果依旧正确,就是可重入函数;重入之后逻辑错乱,就是不可重入。

重入分两种场景:

  1. 多线程导致重入:线程 A 函数跑一半,线程 B 又进入同一个函数
  2. 信号导致重入:函数正在执行,来了信号,信号处理函数再次进入该函数

重点:信号重入是单线程内部发生的,跟多线程没关系,这是考试高频坑。

3、不可重入的典型情况

只要踩下面任意一条,函数就不可重入:

  1. 内部使用全局、static 静态变量;
  2. 调用malloc / free / new,堆管理依靠全局链表;
  3. 调用标准 IO 库printf、fopen等,内部用全局缓冲区;
  4. 返回静态变量的指针。

通俗理解:函数内部有 "公共的、大家共用的缓冲区 / 变量",一旦重入就会把数据搅乱。

  • 可重入:函数中途被打断再进来,结果不能错(重点:信号、递归、并发闯入)
  • 线程安全:多个线程一起跑,共享数据不能被破坏(重点:多线程并发)

锁相关概念:死锁

1 什么是死锁

通俗理解:多个线程互相拿着对方想要的锁,谁也不肯放手,大家全部卡住,谁都没法继续往下跑,程序僵住不动,这就是死锁。

举生活例子:

  • 线程 A:拿到锁 1,还想要锁 2;
  • 线程 B:拿到锁 2,还想要锁 1。

2 死锁四个必要条件

  1. 互斥条件 锁本身就是互斥资源,同一时间只能一个线程持有锁;别人拿不到只能等待。
  2. 请求与保持(占有且等待) 线程已经拿着一把锁,不释放手里的锁,又去申请新的锁

A 拿着 m1,不去释放,又要申请 m2。

  1. 不可剥夺 锁不能被别人强行抢走。只能由持有锁的线程自己主动 unlock 释放,别的线程不能把锁抢过来。
  2. 环路等待 形成等待闭环:A 等 B 的锁,B 又在等 A 的锁,形成循环等待链条。

记住:四个条件全部成立才发生死锁;破坏任意一个,死锁就不会出现。

3 如何避免死锁,对应破坏四个条件

  1. 破坏请求与保持:一次性申请所有锁 线程要用到多把锁时,一次性把需要的全部锁拿到手,不拿着一部分再去申请另一部分;拿不全就一把都不拿。
  2. 破坏环路等待:统一锁的申请顺序(最常用) 所有线程拿锁必须严格按照相同顺序获取锁

不管线程 A 还是线程 B,都必须先拿 m1,再拿 m2。就不会出现 A 拿 m1、B 拿 m2 的闭环。工程中最实用。

  1. 破坏不可剥夺:拿不到锁就主动释放已经持有的锁 如果申请新锁失败,就把手上已经拿到的锁全部释放,过一会儿再重试。调用try lock ,pthread_mutex 本身不支持,需要自己业务控制。
  2. 破坏互斥条件:尽量不用锁 能不用共享资源就不用,使用局部变量、无锁数据结构。很多场景做不到,现实中很少用。

小总结

死锁 = 互斥 + 请求保持 + 不可剥夺 + 环路等待,四者缺一不可。 写代码最容易落地的手段:统一加锁顺序,减少锁嵌套。

STL 容器与智能指针的线程安全

1 STL 容器是不是线程安全?

结论:STL 容器默认不是线程安全,需要我们自己加锁保护。

为什么标准库不帮我们加锁?

STL 追求极致性能。加锁会带来不小的性能开销。 不同业务场景锁的策略也不一样: 比如 unordered_map 哈希表,可以锁整个表,也可以只锁单个桶。标准库不知道我们的业务,没法选最合适的锁策略。 所以 C++ 标准把锁的责任交给使用者自己

STL 容器的安全边界

  1. 多个线程,只读不写(只调用 size、find、at 等查询接口),不做插入、删除、修改,这种情况是安全的。
  2. 只要有一个线程在做写操作(insert、push_back、erase、clear),不管其他线程是读还是写,都不安全,会发生数据竞争。 会出现:程序崩溃、死循环、脏数据、迭代器失效

2 智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题. 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时 候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

相关推荐
棉晗榜18 分钟前
C# HttpClient配置具有tls指纹发起跟浏览器一样的请求解决反爬虫防护
开发语言·爬虫·c#
Sylvia-girl19 分钟前
【一】:Linux基础指令
linux·运维·服务器
0+11144 分钟前
Linux --基础IO
linux·运维·服务器
l1t1 小时前
DeepSeek总结的DuckDB 如何更快地运行递归 CTE
java·开发语言·数据库·mysql·duckdb
HAPPY酷1 小时前
python的对象和方法
开发语言·python
橙露1 小时前
移动端客户端原生开发语言
开发语言
M78佐菲1 小时前
Linux多线程:创建、回收与互斥同步
linux·笔记·学习·算法
Demon--hx1 小时前
设计不能被继承的类
开发语言·c++
兔兔兔兔11 小时前
记录C++ 13
开发语言·c++·算法