ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux之线程池(三)

Linux之线程池(三) 线程池 单例模式为什么线程池要做单例线程池是管理一堆工作线程的资源一个进程只需要一个线程池实例。 如果到处new ThreadPool会创建多组线程抢占 CPU 资源线程爆炸、资源浪费。 所以工程上经常把线程池做成饿汉 / 懒汉单例全局唯一整个程序共用这一组工作线程。本质就是让某一个类对象在内存中只存在一份1.将磁盘加载到内存,创建对象int val100;2.进程在运行期间,创建对象int *val(int *)malloc(sizeof(int));本质就是让某一个类对象在内存中只存在一份(在运行或者加载期间)那么如何做到呢???有懒汉模式和饿汉模式吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭. 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.首先将特定的类的构造函数,拷贝构造,赋值语句全部时private,甚至拷贝构造,赋值语句饿汉方式实现单例模式--加载的时候形成对象template typename T class Singleton { static T data; public: static T* GetInstance() { return data; } };可以优化一下启动速度懒汉方式实现单例模式--在使用的时候才进程创建template typename T class Singleton { static T* inst; public: static T* GetInstance() { if (inst NULL) { inst new T(); } return inst; } };接下来我们来修改一下我们线程池的代码懒汉模式赋值和拷贝构造都要禁止那么我们应该要怎么形成类对象呢??将上面构造变成私有,让外部调用函数来获取构造私有外部无法直接构造对象。删除拷贝、赋值防止复制出第二个实例。懒加载第一次调用Instance()才创建线程池不用就不创建。new 完直接Start()拿到实例就已经启动好线程main 不需要手动调用Start()。在main函数这里就不能用之前的方法ThreadPoolTask模板类指定线程池处理的任务类型是Task::Instance()静态成员函数属于类不是属于某个对象。单例模式静态函数不需要对象直接类名::函数名()调用返回ThreadPoolTask*指针-因为Instance()返回的是对象指针访问成员函数要用箭头-不能用.Enqueue(t)线程池的入队接口把任务放进任务队列。因为单例模式下构造函数被 private 私有化外部不能直接ThreadPoolTask pool;创建局部对象。整个程序只能有唯一一个线程池对象所有地方都必须通过Instance()拿到那唯一的实例指针。重点Instance()是静态函数没有对象也可以调用它内部 new 出唯一对象返回对象的地址指针。拿到指针之后用-调用普通成员Enqueue、Stop、Wait。现在完成了最简单的方法来实现,但是这里依然存在着很多的问题问题只有第一次初始化的时候并发会出事_instance不为 nullptr 之后直接 return完全安全。线程 A判断_instance nullptr→ true进入 if时间片切走线程 B同样判断_instance nullptr→ true也进入 if线程 B 执行new ThreadPoolT()_instance被赋值第一个对象地址切回线程 A继续执行new ThreadPoolT()又 new 第二个对象覆盖_instance结果内存泄漏第一个 new 出来的对象丢失出现两个线程池实例系统里多组工作线程业务完全错乱。后续再调用 Instance_instance已经不为空直接 return不会再触发 new所以后续调用没问题。所以现在还需要优化加锁因为我们在处理完一次之后就不用在加锁也不会多创建对象了接下来我们就可以采用if判断语句来进行优化线程安全与可重入函数很多人容易把线程安全和可重入搞混二者有关系但不完全等价。1、什么是线程安全多个线程同时跑同一个函数、访问同一份共享数据程序不会乱、不会出现奇怪结果就叫线程安全。如果函数里面只用局部变量栈上每个线程自己一份基本不会出问题。如果操作全局变量 /static 静态变量又不加锁保护大概率线程不安全。线程安全解决的是多线程并发访问共享资源会不会搞坏数据。2、什么是可重入函数一个函数还没跑完又被再次调用进来这种现象就叫重入。 重入之后结果依旧正确就是可重入函数重入之后逻辑错乱就是不可重入。重入分两种场景多线程导致重入线程 A 函数跑一半线程 B 又进入同一个函数信号导致重入函数正在执行来了信号信号处理函数再次进入该函数重点信号重入是单线程内部发生的跟多线程没关系这是考试高频坑。3、不可重入的典型情况只要踩下面任意一条函数就不可重入内部使用全局、static 静态变量调用malloc / free / new堆管理依靠全局链表调用标准 IO 库printf、fopen等内部用全局缓冲区返回静态变量的指针。通俗理解函数内部有 “公共的、大家共用的缓冲区 / 变量”一旦重入就会把数据搅乱。可重入函数中途被打断再进来结果不能错重点信号、递归、并发闯入线程安全多个线程一起跑共享数据不能被破坏重点多线程并发锁相关概念死锁1 什么是死锁通俗理解多个线程互相拿着对方想要的锁谁也不肯放手大家全部卡住谁都没法继续往下跑程序僵住不动这就是死锁。举生活例子线程 A拿到锁 1还想要锁 2线程 B拿到锁 2还想要锁 1。2 死锁四个必要条件互斥条件锁本身就是互斥资源同一时间只能一个线程持有锁别人拿不到只能等待。请求与保持占有且等待线程已经拿着一把锁不释放手里的锁又去申请新的锁。A 拿着 m1不去释放又要申请 m2。不可剥夺锁不能被别人强行抢走。只能由持有锁的线程自己主动 unlock 释放别的线程不能把锁抢过来。环路等待形成等待闭环A 等 B 的锁B 又在等 A 的锁形成循环等待链条。记住四个条件全部成立才发生死锁破坏任意一个死锁就不会出现。3 如何避免死锁对应破坏四个条件破坏请求与保持一次性申请所有锁线程要用到多把锁时一次性把需要的全部锁拿到手不拿着一部分再去申请另一部分拿不全就一把都不拿。破坏环路等待统一锁的申请顺序最常用所有线程拿锁必须严格按照相同顺序获取锁。不管线程 A 还是线程 B都必须先拿 m1再拿 m2。就不会出现 A 拿 m1、B 拿 m2 的闭环。工程中最实用。破坏不可剥夺拿不到锁就主动释放已经持有的锁如果申请新锁失败就把手上已经拿到的锁全部释放过一会儿再重试。调用try lock ,pthread_mutex 本身不支持需要自己业务控制。破坏互斥条件尽量不用锁能不用共享资源就不用使用局部变量、无锁数据结构。很多场景做不到现实中很少用。小总结死锁 互斥 请求保持 不可剥夺 环路等待四者缺一不可。 写代码最容易落地的手段统一加锁顺序减少锁嵌套。STL 容器与智能指针的线程安全1 STL 容器是不是线程安全结论STL 容器默认不是线程安全需要我们自己加锁保护。为什么标准库不帮我们加锁STL 追求极致性能。加锁会带来不小的性能开销。 不同业务场景锁的策略也不一样 比如 unordered_map 哈希表可以锁整个表也可以只锁单个桶。标准库不知道我们的业务没法选最合适的锁策略。 所以 C 标准把锁的责任交给使用者自己。STL 容器的安全边界多个线程只读不写只调用 size、find、at 等查询接口不做插入、删除、修改这种情况是安全的。只要有一个线程在做写操作insert、push_back、erase、clear不管其他线程是读还是写都不安全会发生数据竞争。 会出现程序崩溃、死循环、脏数据、迭代器失效2 智能指针是否是线程安全的?对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题. 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时 候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
返回列表