单例及线程池的实现及感悟分享

devtools/2025/1/21 12:56:07/

        

碎碎念:

        有快3个月没有写博客了,这段时间很多事,抽不出空来写博客。后面的博客可能风格也会转变,从理论到实战为主,尽量减少源码的解读。(看完源码后面自己也忘了QAQ),本篇博客主要介绍线程池的实现及其前置单例模式的一些实现方法,其中使用了恋恋辰风大佬的一些例子。

单例模式

        在众多的设计模式中,尽管因为语言特性而使某些设计模式缺乏实用性。但是单例模式却是使用最为广泛的设计模式之一。在需要大规模数据通信的场景,又或者需要重复构造释放以及各种工厂模式,句柄。单例模式都在其中展现出了优势。例如,如果你想进行IPC通信,如果仅仅是处理轻量级的数据交换或同步任务,那么你可以选择使用future,task,promise等,但是如果是大规模数据,实现一个单例缓冲区Buffer,构造一个生产者消费者模型则是方法之一。值得一提的是,在观看boost库的实现中,我发现了一种写法可以些许提高性能,也就是消费者自身维护一个buffer,在消费完之后,直接和单例类中的buffer进行swap,而无需从单例buffer中取数据,这种方法可以减少锁的竞争和访问延迟。。单例模式一般常见的来说有懒汉式和饿汉式两种,懒汉式就是在getInstance函数执行的时候如果未创建单例,则创建单例。饿汉式则是将单例模式的创建放在了构造函数当中。如果是单线程无所谓,但是在多线程的情况下,我并不是很推荐使用懒汉式(即使他使用双锁)。这是因为new本身不是一个原子操作,他可能出现重排序问题从而先返回了但是还没实际构建,这样的风险非常大。而懒汉式由于是在构造函数中实现则不存在这个问题。这两种我都不是很常用,我常用的两种一种是返回局部变量,这种非常简单,而且可靠性较高,但是注意不要在C++11标准之前使用,会有风险,而11之后修复了这个问题。第二种是使用once_flag和call_once配合来实现,call_once会保证once_flag只被初始化一次。下面来看一下具体的实现,为了观看方便我就不分开写了:

#include<mutex>
#include<memory>class SingleClass {public:static SingleClass& getInstance() {static SingleClass instance;return instance;}static std::shared_ptr<SingleClass> getInstance2() {std::call_once(flag_, [&]() {instance_ = std::make_shared<SingleClass>();});return instance_;}
private:static std::once_flag flag_;SingleClass() = default;SingleClass(const SingleClass&) = delete;SingleClass& operator =(const SingleClass&) = delete;static std::shared_ptr<SingleClass>  instance_;
};std::once_flag SingleClass::flag_;
std::shared_ptr<SingleClass> SingleClass::instance_ = nullptr;

        这里有几个注意的点,getInstance都要返回引用,一定一定一定要返回引用,不然他就是走的拷贝,我们已经删除了就过不了而且就算是过了也不能走拷贝。

        如果需要单例多可以写个模板:

#pragma once
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;template<typename T>
class Singleton{public:static shared_ptr<T> getInstance(){static std::once_flag s_flag;std::call_once(s_flag,[&]{instance_ = make_shared<T>();});return instance_;}protected:Singleton()=default;Singleton(const Singleton& ) = delete;Singleton& operator= (const Singleton& ) = delete;static shared_ptr<T> instance_; 
};
template<typename T>
shared_ptr<T> Singleton<T>::instance_ = nullptr;

这个只要继承就好了,他比较好的就是继承之后不用删除拷贝构造和重写赋值了,因为构造由内而外,先构造父类,父类那里直接禁了子类也用不了这俩了。但是需要注意的是,如果子类自身的构造函数放在private里记得把这个模板设为友元,不然拿不到构造函数。

线程池

        线程池的主要功能是管理线程的生命周期,以减少频繁创建和销毁线程的开销。在线程池创建时,会启动一定数量的线程,并让这些线程通过任务队列来获取待执行的任务。工作线程会不断地循环获取任务并执行,从而提高了任务处理的效率。线程池在销毁时,确保所有子线程都完成任务后才退出,这一点非常重要。如果主线程在子线程还未完成任务时就退出,可能会导致未完成的任务丢失或程序状态不一致。因此,在设计线程池时,必须确保在主线程退出之前,所有子线程的任务都已经处理完毕。在多线程程序设计中,通常需要确保主线程在所有子线程完成任务后才退出。线程池通过管理工作线程的生命周期和同步机制(如使用`join`或条件变量)确保所有任务完成后,主线程才能安全退出。我们来看一下主要的实现,这里使用了恋恋辰风博主写的一个线程池,只完成了主要功能。也便于理解:

线程池主要代码

#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__#include <atomic>
#include <condition_variable>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>class NoneCopy {public:~NoneCopy(){}
protected:NoneCopy(){}
private:NoneCopy(const NoneCopy&) = delete;NoneCopy& operator=(const NoneCopy&) = delete;
};class ThreadPool : public NoneCopy {
public://继承基类NoneCopy就不需要写如下删除了//ThreadPool(const ThreadPool&) = delete;//ThreadPool& operator=(const ThreadPool&) = delete;static ThreadPool& instance() {static ThreadPool ins;return ins;}using Task = std::packaged_task<void()>;~ThreadPool() {stop();}template <class F, class... Args>auto commit(F&& f, Args&&... args) -> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));if (stop_.load())return std::future<RetType>{};auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<RetType> ret = task->get_future();{std::lock_guard<std::mutex> cv_mt(cv_mt_);tasks_.emplace([task] { (*task)(); });}cv_lock_.notify_one();return ret;}int idleThreadCount() {return thread_num_;}private:ThreadPool(unsigned int num = std::thread::hardware_concurrency()): stop_(false) {{if (num <= 1)thread_num_ = 2;elsethread_num_ = num;}start();}void start() {for (int i = 0; i < thread_num_; ++i) {pool_.emplace_back([this]() {while (!this->stop_.load()) {Task task;{std::unique_lock<std::mutex> cv_mt(cv_mt_);this->cv_lock_.wait(cv_mt, [this] {return this->stop_.load() || !this->tasks_.empty();});if (this->tasks_.empty())return;task = std::move(this->tasks_.front());this->tasks_.pop();}this->thread_num_--;task();this->thread_num_++;}});}}void stop() {stop_.store(true);cv_lock_.notify_all();for (auto& td : pool_) {if (td.joinable()) {std::cout << "join thread " << td.get_id() << std::endl;td.join();}}}private:std::mutex               cv_mt_;std::condition_variable  cv_lock_;std::atomic_bool         stop_;std::atomic_int          thread_num_;std::queue<Task>         tasks_;std::vector<std::thread> pool_;
};#endif  // !__THREAD_POOL_H__

       继承

class NoneCopy {public:~NoneCopy(){}
protected:NoneCopy(){}
private:NoneCopy(const NoneCopy&) = delete;NoneCopy& operator=(const NoneCopy&) = delete;
};

        这里继承也就是我们前面说的模板继承方式,不过他并没有在模板类中创建getinsance而是禁止拷贝构造和拷贝赋值。往下看

    static ThreadPool& instance() {static ThreadPool ins;return ins;}using Task = std::packaged_task<void()>;~ThreadPool() {stop();}

        第一个没什么好说的 ,析构也没什么好说的,主要看第二个Task,他可不是乱写的哈,这里是因为投递的任务返回类型不同,参数也不同统一拿void()包装一下,内部函数就是通过Bind绑定过参数的任务了,直接调用就好了。对了这里要给不太了解的朋友说一下,这个packaged_task可以和future绑定,只要返回future,使用线程池调用者就可以通过future拿到函数运行后的返回值。接下来看看构造函数:

构造函数

ThreadPool(unsigned int num = std::thread::hardware_concurrency()): stop_(false) {{if (num <= 1)thread_num_ = 2;elsethread_num_ = num;}start();}

        这里也没什么好说的,建议是开的线程数等于你的CPU核数。过多的线程会造成线程竞争,浪费时间片。但是1核我们也不能让他太寒碜变成单线程了吧,至少开两个吧。。。

 start函数

    void start() {for (int i = 0; i < thread_num_; ++i) {pool_.emplace_back([this]() {while (!this->stop_.load()) {Task task;{std::unique_lock<std::mutex> cv_mt(cv_mt_);this->cv_lock_.wait(cv_mt, [this] {return this->stop_.load() || !this->tasks_.empty();});if (this->tasks_.empty())return;task = std::move(this->tasks_.front());this->tasks_.pop();}this->thread_num_--;task();this->thread_num_++;}});}}

        这也就是线程就射设置线程,让他循环的执行任务,{}作用域的作用主要是提前结束对共享资源的掌控,要是想用延迟锁也可以,直接让他加完锁再解锁,只是这种方便一点。而且对于lock_guard这种不支持加锁解锁的,也可以控制加锁范围。非常方便十分推荐。这里需要注意的是如果他等待挂起的时候如果是准备退出了,那么他就处理完自己的函数就退出了。忘了说了这里一定要用emlace_back。因为push_back其实是拷贝,那就会存在两个线程一个还回收不掉。

stop函数

    void stop() {stop_.store(true);cv_lock_.notify_all();for (auto& td : pool_) {if (td.joinable()) {std::cout << "join thread " << td.get_id() << std::endl;td.join();}}}

       这里也没什么好说的,就是设置退出标志服,然后唤醒所有挂起线程,再回收。

Commit函数

        这是线程池中最关键的函数,为什么不同返回值什么不同的参数函数可以投递到一个队列中.我们来看一下。

template <class F, class... Args>auto commit(F&& f, Args&&... args) -> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {using RetType = decltype(std::forward<F>(f)(std::forward<Args>(args)...));if (stop_.load())return std::future<RetType>{};auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<RetType> ret = task->get_future();{std::lock_guard<std::mutex> cv_mt(cv_mt_);tasks_.emplace([task] { (*task)(); });}cv_lock_.notify_one();return ret;}

        首先他使用了auto来确定返回值的类型,怎么做到的呢?

auto commit(F&& f, Args&&... args) 
-> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))>

这个首先他返回的是一个future嘛,这样调用者才可以通过future拿到结果。里面就是用decltype确定类型,什么类型呢?f函数调用的类型。后面args是参数,...是展开。这样就可以输入多个参数然后确定类型了。注意这里一定要用完美转发来确保他的参数左值右值性质不变。往后就是如果线程准备退出了就直接return。

        auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<RetType> ret = task->get_future();{std::lock_guard<std::mutex> cv_mt(cv_mt_);tasks_.emplace([task] { (*task)(); });}

        然后task用bind进行绑定参数,这样就可以不输入参数直接调用了,但是他还有返回值怎么办呢?那就用lambda表达式直接包装一层就行了,注意这里一定要捕获task要不就&,最好不要捕获this。不然会导致潜在的访问悬空指针的问题,除非你确保this在任务执行期间依然有效。就算他是单例模式,在主程序挂掉之前不会失效,我仍然还是建议不要捕获this。然后往下直接唤醒返回就行了。

结语

        放假之后抽空写了一下自己的一些理解和感悟,希望后面能找一个好一点的实习。后面主要会写车载网络的一些知识,也穿插一下和普通TCP/IP的区别,DDS和SOME/IP的一些实战。源码还是等工作后有空再写吧。


http://www.ppmy.cn/devtools/152351.html

相关文章

TinyEngine v2.1版本发布:全新的区块方案和画布通信方案,打造更强力的可拓展低代码引擎

前言 2025年蛇年已经到来&#xff0c;TinyEngine v2.1.0 版本也已经蛇气腾腾的发布了出来&#xff0c;新年新气象&#xff0c;为了让大家更详细了解到 v2.1.0 的内容更新&#xff0c;我们特此列举了该版本中的一些重要特性更新。 v2.1.0变更特性概览 1、使用了新的纯前端区块…

MIAOYUN信创云原生项目亮相西部“中试”生态对接活动

近日&#xff0c;以“构建‘中试’生态&#xff0c;赋能科技成果转化”为主题的“科创天府智汇蓉城”西部“中试”生态对接活动在成都高新区菁蓉汇隆重开幕。活动分为成果展览、“中试”生态主场以及成果路演洽谈对接三大板块。在成果展览环节&#xff0c;成都元来云志科技有限…

OpenHarmony API 设计规范

OpenHarmony API 设计规范 修订记录 版本作者时间更新内容v0.1&#xff0c;试运行版OpenHarmony API SIG2022年11月初版发布 目的 API是软件实现者提供给使用者在编程界面上的定义&#xff0c;API在很大程度上体现了软件实体的能力范围。 同时&#xff0c;API定义的好坏极…

《鸿蒙 HarmonyOS 应用开发从入门到精通(第 2 版)》学习笔记 ——HarmonyOS 环境搭建之安装DevEco Studio

作为一款开发工具&#xff0c;除了具有基本的代码开发、编译构建及调测等功能外&#xff0c;DevEco Studio还具有如下特点&#xff1a; 高效智能代码编辑&#xff1a;支持Java、XML、ArkTS、JS、C/C等语言的代码高亮、代码智能补齐、代码错误检查、代码自动跳转、代码格式化、…

[Unity]【游戏开发】 脚本创建物体的实践与技巧

在Unity游戏开发中,动态创建物体是一个常见的需求。为了提高开发效率并实现灵活的物体生成,开发者通常会利用预制体来作为物体的模板,然后通过脚本在运行时动态创建物体。本文将详细讲解如何通过脚本创建物体,并涵盖一些常见的技巧和方法。 预制体与实例化 预制体简介 预…

深入理解机器学习中的零样本、少样本与微调

在机器学习领域&#xff0c;特别是在大语言模型&#xff08;LLM&#xff09;的评估中&#xff0c;我们经常听到zero-shot&#xff08;零样本&#xff09;、few-shot&#xff08;少样本&#xff09;和fine-tuning&#xff08;微调&#xff09;这些术语。这篇文章将通过具体示例来…

什么是馈线自动化(FA)?其优点是什么?本文给出答案

馈线自动化&#xff08;FA&#xff0c;Feeder Automation&#xff09;&#xff0c;即配电线路自动化&#xff0c;是指配电自动化系统对配电线路故障进行分析定位&#xff0c;并执行故障隔离和非故障区段恢复供电的操作。它是配网系统自动化的一个重要组成部分&#xff0c;具体内…

SQL刷题快速入门(三)

其他章节&#xff1a; SQL刷题快速入门&#xff08;一&#xff09; SQL刷题快速入门&#xff08;二&#xff09; 承接前两个章节&#xff0c;本系列第三章节主要讲SQL中where和having的作用和区别、 GROUP BY和ORDER BY作用和区别、表与表之间的连接操作&#xff08;重点&…