【Linux笔记】——线程池项目与线程安全单例模式

news2025/5/22 6:22:59

🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹: 【Linux笔记】——简单实习一个日志项目
🔖流水不争,争的是滔滔不息


  • 一、线程池设计
  • 二、线程池代码
  • 三、线程安全的单例模式
  • 四、线程安全和重入问题

一、线程池设计

线程池

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度,可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。线程池的主要优点是减少在创建和销毁线程上所花的时间以及系统资源的开销。通过重用已存在的线程,线程池可以显著提高系统性能,特别是在需要处理大量短生命周期任务的场景中。

使用场景

需要大量的线程来完成任务,且完成任务的时间比较短。比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

对性能要求苛刻的应用,比如要求服务器迅速响应客户的请求。

接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

在这里插入图片描述

二、线程池代码

ThreadPool.hpp线程池主体逻辑

#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace std;
using namespace MutexModule;
using namespace CondModule;
using namespace ThreadModule;
using namespace LogModule;

const int gnum=5;
namespace ThreadPoolModule
{
    template<class T>
    class ThreadPool
    {
    public:
        ThreadPool(int num=gnum)
        :_num(num)
        ,_isrunning(false)
        ,_sleepnum(0)
        {
            for(int i=0;i<_num;i++)
            {
                _thread.emplace_back([this](){
                    HandlerTask();
                });
            }
        }

        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }

        void Threadall()
        {
            LockGuard lockguard (_mutex);
            if(_sleepnum>0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel :: INFO)<<"唤醒所有休眠线程";
        }

        void Start()
        {
            if(_isrunning) return;
            _isrunning=true;
            for(auto &thread :_thread)
            {
                thread.Start();
            }
            LOG(LogLevel :: INFO)<<"开始创建线程池";
        }
  
        void Stop()
        {
            if(!_isrunning) return;
            _isrunning =false;
            Threadall();//让等待的进程全部启动
        }

        void Join()
        {
            for(auto &thread :_thread)
            {
                thread.Join();
            }
            LOG(LogLevel :: INFO)<<"线程回收";
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }
            
        }

        bool Enqueue (const T& in)
        {
            if(_isrunning)
            {
                LockGuard LockGuard (_mutex);
                _taskq.push(in);
                if(_sleepnum==_thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;    

        }

        ~ThreadPool()
        {

        }

    private:
        vector<Thread> _thread;
        queue<T> _taskq;
        int _num;
        Mutex _mutex;
        Cond _cond;
        bool _isrunning;
        int _sleepnum;

    };
}

这里用到了之前封装好的线程、条件变量互斥与同步、日志。

私有成员变量
_thread我们用vector数组充当线程池,_taskq任务队列用的是queue队列,_num是线程池中的线程的个数(我们写的是固定线程的线程池),_isrunning判断线程是否运行,_sleepnum是线程等待的个数。


		ThreadPool(int num=gnum)
        :_num(num)
        ,_isrunning(false)
        ,_sleepnum(0)
        {
            for(int i=0;i<_num;i++)
            {
                _thread.emplace_back([this](){
                    HandlerTask();
                });
            }
        }
        
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }

上面的代码是构造函数构造线程池,这里主要阐述,构造函数创建线程池与线程去执行任务的函数的关系。
构造函数通过一个for循环,我们创建了_num个Thread对象,每个对象都绑定一个lambda,lambda里面调用的是线程池的成员函数HandlerTask()(在类内调用类内成员函数用lambda),这些lambda是“线程的入口函数”,它们一启动就跑进HandlerTask()中并一直在那里循环干活。


void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while(true)
            {
                T t;
                {
                    LockGuard lockguard (_mutex);

                    if(_taskq.empty() && _isrunning) //把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++;//如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--;//退出等待计数--
                    }

                    if(_taskq.empty() && !_isrunning) //等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel :: INFO)<<name<<"退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t=_taskq.front();
                    _taskq.pop();
                }
                t();//执行任务
            }

上面这段代码。一启动,线程池的每个线程都会进去,所以加锁。如果任务队列是空的并且这个线程池已经启动了,那么线程就要进入等待队列(条件变量同步),注意要写一个计数的变量需要进入等待队列就要++出来就–(这里+±-就是为了唤醒休眠线程的时候进行判断,颗粒度更细)。如果任务队列为空并且线程池也不在运行了,直接break退出回收线程。最后满足线程能拿到任务,取出队首的任务,执行任务。


		//唤醒单个线程
        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }
		//唤醒所有进程
        void Threadall()
        {
            LockGuard lockguard (_mutex);
            if(_sleepnum>0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel :: INFO)<<"唤醒所有休眠线程";
        }
		//创建线程池
        void Start()
        {
            if(_isrunning) return;
            _isrunning=true;
            for(auto &thread :_thread)
            {
                thread.Start();
            }
            LOG(LogLevel :: INFO)<<"开始创建线程池";
        }
  		//终止线程
        void Stop()
        {
            if(!_isrunning) return;
            _isrunning =false;
            Threadall();//让等待的进程全部启动
        }
		//回收线程
        void Join()
        {
            for(auto &thread :_thread)
            {
                thread.Join();
            }
            LOG(LogLevel :: INFO)<<"线程回收";
        }
        //任务队列中放任务
		bool Enqueue (const T& in)
        {
            if(_isrunning)
            {
                LockGuard LockGuard (_mutex);
                _taskq.push(in);
                if(_sleepnum==_thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;    

        }

唤醒单个线程直接调用之前封装好的条件变量同步,唤醒所有休眠的线程,如果之前计数的_sleepnum>0就要唤醒所有的休眠线程了。
启动线程就是创建vector数组里的线程,创建线程池。
进程终止,调用之前封装的条件变量。
回收线程,调用之前封装的条件变量。

Enqueue就是往任务队列里放任务,if(_sleepnum==_thread.size()) 判断是否所有线程都在休眠,避免唤醒线程池中已经在忙的线程(节省上下文切换开销)如果不判断 _sleepnum,直接 Threadone() 会怎样?可能会导致重复唤醒甚至无意义的上下文切换。比如:有5个线程,其中2个在处理任务,3个在睡觉;你来了个新任务,就唤醒1个线程;但其实可能原本某个线程马上就处理完会抢新任务;你提前唤醒一个,就多了一次线程上下文切换(白唤醒了)。


main.cc

#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

int main()
{
    Enable_Console_Log_Strategy();

    int cnt=5;
    ThreadPool<task_t>* tp=new ThreadPool <task_t> ();
    tp->Start();
    
    while(cnt)
    {
        sleep(1);
        tp->Enqueue(Download);
        cnt--;
    }
   
    tp->Stop();
    tp->Join();
    return 0;
}

Task.hpp

#pragma once
#include <iostream>
#include <functional>
#include "Log.hpp"

using namespace LogModule;

using task_t = std::function<void()>;

void Download()
{
   LOG(LogLevel::DEBUG) << "我是一个下载任务...";
}

这里的function就简单写了一下,没有放具体的任务,但是要走到这里用的是function的语法


线程池就是通过一个vector数组(这里这么写的也可以是别的)里面创建线程,其实就是把线程准备好放在vector数组中,任务来了线程直接就能用,大大提高了效率。线程池就是提前创建一批线程放在池子里反复复用,避免任务来了才临时创建/销毁线程造成的高开销。

在这里插入图片描述

常规线程池:源码

三、线程安全的单例模式

单例模式

单例模式是一种设计模式,确保一个类只有一个实例(对象),并提供一个全局访问点来获取该实例。这种模式通常用于控制资源的访问,例如数据库连接、日志记录器等,以避免创建多个实例导致资源浪费或冲突。

饿汉模式实现单例
一开始就创建好单例对象。程序一启动,就创建好对象了,饿的不行

懒汉模式实现单例
什么时候用什么时候创建单例对象。防止创建多个对象。第一次用对象是才创建,是不是很懒。


线程安全的懒汉式单例模式

禁用拷贝构造和赋值重载

ThreadPool(const ThreadPool<T> &) = delete;               // 把拷贝构造给禁用 没办法直接创建对象
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 把赋值重载禁用

显示的禁掉拷贝构造和赋值重载,单例模式的最大的特征以及核心就是,保证全程只有一个对象。如果不禁掉拷贝构造和赋值重载就会出现多个对象,完全违背了单例迷失的初衷。
没有禁用拷贝构造和赋值重载,单例根本不单例。


类外初始化单例指针

template <class T>
class ThreadPool {
public:
    static ThreadPool<T>* inc;
    // 其他成员...
};


template <class T>
ThreadPool<T>* ThreadPool<T>::inc = nullptr;// 类外初始化

template <class T>
Mutex ThreadPool<T>::_lock; // 类外初始化

类内声明的静态成员变量,必须类外初始化。 static ThreadPool* inc这个静态成员变量必须类外初始化。原因是:静态成员变量属于类本身,而不是某个对象。类声明只是告诉编译器"这里有这么个静态变量",但不分配内存。只有在类外定义后,编译器才会给它分配内存空间。

为什么要加static成为静态成员变量?


创建单例对象

static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr) // 多加一层多一层保护  
            {
                LockGuard lockguard(_lock); // 为防止多线程访问,加锁
                if (inc == nullptr) //第一次用这个inc单例指针对象
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>();  //创建单例指针对象
                }
            }

            return inc;
        }

这里主要就是创建单例指针对象,如果这里不加static如果想调用这个函数是不是就要创建这个类的对象就违背了单例的初衷。所以这里要让它成为静态成员函数。这也回答了上面为什么要用static成员变量,在这个静态成员函数中创建单例指针对象,要使用静态成员变量。
内层中的if (inc == nullptr)是判断是否是第一次用这个inc单例指针对象,如果没有创建单例指针对象。加锁是为了防止多线程访问。在外层的if (inc == nullptr),首先不加外层的这个if (inc == nullptr),线程来了访问互斥锁,拿到锁的线程进去创建单例指针对象,那么多线程是不是每次不管是否已经创建了单例指针对象都要拿锁然后进去转一圈在返回这个单例指针对象,是不是效率就会降低。加上最外侧的if (inc == nullptr) 每次线程来了,如果已经有单例指针对象了就不要进去在转一圈,直接返回对象就完了,大大提高了效率。


Main.cc

#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

int main()
{
    Enable_Console_Log_Strategy();//

    int cnt=5;
    // ThreadPool<task_t>* tp=new ThreadPool <task_t> ();
    ThreadPool<task_t> ::GetInstance()->Start();
    
    while(cnt)
    {
        sleep(1);
        ThreadPool<task_t> ::GetInstance()->Enqueue(Download);
        cnt--;
    }
   
    ThreadPool<task_t> ::GetInstance()->Stop();
    ThreadPool<task_t> ::GetInstance()->Join();
    return 0;
}

单例模式如何访问类内成员函数呢?不用创建类的对象,直接用作用域解释符,调用静态成员函数。这样获取进程池的唯一对象,然后通过指针调用方法创建线程池。

懒汉式单例模式线程池

#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
using namespace std;
using namespace MutexModule;
using namespace CondModule;
using namespace ThreadModule;
using namespace LogModule;

const int gnum = 5;
namespace ThreadPoolModule
{
    template <class T>
    class ThreadPool
    {
    private:
        ThreadPool(int num = gnum)
            : _num(num), _isrunning(false), _sleepnum(0)
        {
            for (int i = 0; i < _num; i++)
            {
                _thread.emplace_back([this]()
                                     { HandlerTask(); });
            }
        }

        void Threadone()
        {
            _cond.Signal();
            LOG(LogLevel::INFO) << "唤醒一个休眠线程";
        }

        void Threadall()
        {
            LockGuard lockguard(_mutex);
            if (_sleepnum > 0)
            {
                _cond.Broadcast();
            }
            LOG(LogLevel ::INFO) << "唤醒所有休眠线程";
        }

        ThreadPool(const ThreadPool<T> &) = delete;               // 把拷贝构造给禁用 没办法直接创建对象
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 把赋值重载禁用
    public:
        
        static ThreadPool<T> *GetInstance()
        {
            if (inc == nullptr) // 多加一层多一层保护  
            {
                LockGuard lockguard(_lock); // 为防止多线程访问,加锁
                if (inc == nullptr) //第一次用这个inc单例指针对象
                {
                    LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
                    inc = new ThreadPool<T>();  //创建单例指针对象
                }
            }

            return inc;
        }

        void Start()
        {
            if (_isrunning)
                return;
            _isrunning = true;
            for (auto &thread : _thread)
            {
                thread.Start();
            }
            LOG(LogLevel ::INFO) << "开始创建线程池";
        }

        void Stop()
        {
            if (!_isrunning)
                return;
            _isrunning = false;
            Threadall(); // 让等待的进程全部启动
        }

        void Join()
        {
            for (auto &thread : _thread)
            {
                thread.Join();
            }
            LOG(LogLevel ::INFO) << "线程回收";
        }
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T t;
                {
                    LockGuard lockguard(_mutex);

                    if (_taskq.empty() && _isrunning) // 把全部休眠的线程启动,必须保证进程池已经退出状态,要不就陷入死循环
                    {
                        _sleepnum++; // 如果等待计数++
                        _cond.Wait(_mutex);
                        _sleepnum--; // 退出等待计数--
                    }

                    if (_taskq.empty() && !_isrunning) // 等待后唤醒,必须是任务队列为空 进程池退出
                    {
                        LOG(LogLevel ::INFO) << name << "退出了,任务队列为空,进程池退出";
                        break;
                    }

                    t = _taskq.front();
                    _taskq.pop();
                }
                t(); // 执行任务
            }
        }

        bool Enqueue(const T &in)
        {
            if (_isrunning)
            {
                LockGuard LockGuard(_mutex);
                _taskq.push(in);
                if (_sleepnum == _thread.size())
                {
                    Threadone();
                }
                return true;
            }
            return false;
        }

        ~ThreadPool()
        {
        }

    private:
        vector<Thread> _thread;
        queue<T> _taskq;
        int _num;
        Mutex _mutex;
        Cond _cond;
        bool _isrunning;
        int _sleepnum;
        static ThreadPool<T> *inc;
        static Mutex _lock;
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::inc = nullptr; // 类外初始化

    template <class T>
    Mutex ThreadPool<T>::_lock; // 类外初始化
}

运行结果
在这里插入图片描述
懒汉式单例模式线程池:源码

四、线程安全和重入问题

线程安全:就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。⼀般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

重入:同⼀个函数被不同的执行流调用,当前⼀个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。⼀个函数在重⼊的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。学到现在,其实我们已经能理解重入其实可以分为两种情况1.多线程重入函数2.信号导致⼀个执行流重复进入函数


可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的(其实知道这⼀句话就够了)
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果⼀个函数中有全局变量,那么这个函数既不是线程安全也不是可重人的。

可重入与线程安全区别

  • 可重入函数是线程安全函数的⼀种
  • 线程安全不一定是可重入的,而可重入函数则⼀定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角度不做区分。
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点。
可重入描述的是一个函数是否被重复进入,表示的是函数特点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2382138.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ZooKeeper 原理解析及优劣比较

大家好&#xff0c;这里是架构资源栈&#xff01;点击上方关注&#xff0c;添加“星标”&#xff0c;一起学习大厂前沿架构&#xff01; 引言 在分布式系统中&#xff0c;服务注册、配置管理、分布式锁、选举等场景都需要一个高可用、一致性强的协调服务。Apache ZooKeeper 凭…

是德科技 | 单通道448G未来之路:PAM4? PAM6? PAM8?

内容来源&#xff1a;是德科技 随着数据中心规模的不断扩大以及AI大模型等技术的兴起&#xff0c;市场对高速、大容量数据传输的需求日益增长。例如&#xff0c;AI训练集群中GPU等设备之间的互联需要更高的传输速率来提升效率。在技术升级方面&#xff0c;SerDes技术的不断进步…

OceanBase 开发者大会,拥抱 Data*AI 战略,构建 AI 数据底座

5 月 17 号以“当 SQL 遇见 AI”为主题的 OceanBase 开发者大会在广州举行&#xff0c;因为行程的原因未能现场参会&#xff0c;仍然通过视频直播观看了全部的演讲。总体来说&#xff0c;这届大会既有对未来数据库演进方向的展望&#xff0c;也有 OceanBase 新产品的发布&#…

STM32IIC协议基础及Cube配置

STM32IIC协议基础及Cube配置 一&#xff0c;IC协议简介1&#xff0c;核心特点2&#xff0c;应用场景 二&#xff0c;IC协议基础概念1&#xff0c;总线结构2&#xff0c;主从架构3&#xff0c;设备寻址4&#xff0c;起始和停止条件5&#xff0c;数据传输6&#xff0c;应答机制 三…

CNN vs ViT:图像世界的范式演进

一、图像建模&#xff0c;是不是也可以“大一统” 在前文中我们提到&#xff0c;多模态大模型打破“只能处理文字”的限制。 在 NLP 世界里&#xff0c;Transformer 已经证明自己是理解语言的王者。那么在图像世界&#xff0c;我们是否也能有一种“通用架构”&#xff0c;让模…

cocos creator使用jenkins打包微信小游戏,自动上传资源到cdn,windows版运行jenkins

cocos 版本2.4.11 在windows上jenkins的具体配置和部署&#xff0c;可参考上一篇文章cocos creator使用jenkins打包流程&#xff0c;打包webmobile_jenkins打包,发布,部署cocoscreator-CSDN博客 特别注意&#xff0c;windows上运行jenkins需要关闭windows自己的jenkins服务&a…

定时器的两种实现方式

1、基于优先级队列/堆 队列是先进先出&#xff0c;优先级队列是优先级越高就存放在队列之前&#xff0c;我们可以将过期时间越早设置为优先级越高&#xff0c;那么临近过期时间的任务就会在队列前面&#xff0c;距离过期时间越晚的任务就在队列后面。 可以分配一个线程&#…

[Java实战]Spring Boot整合MinIO:分布式文件存储与管理实战(三十)

[Java实战]Spring Boot整合MinIO&#xff1a;分布式文件存储与管理实战&#xff08;三十&#xff09; 一、MinIO简介与核心原理 MinIO 是一款高性能、开源的分布式对象存储系统&#xff0c;兼容 Amazon S3 API&#xff0c;适用于存储图片、视频、日志等非结构化数据。其核心特…

AI在人力资源领域的应用:把握时代浪潮

借鉴历史经验&#xff0c;引领技术变革 历史总是呈现出惊人的相似性。十年前&#xff0c;众多企业未能及时洞察移动技术与社交技术的潜在价值&#xff0c;迟迟没有将这些创新引入职场环境。随着时间推移&#xff0c;这些组织才意识到BYOD&#xff08;自带设备办公&#xff09;…

vr制作公司提供什么服务?

随着科技的迅猛进步&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;技术已经悄然渗透到我们的日常生活与工作中&#xff0c;成为推动数字化转型的重要力量。VR制作公司&#xff0c;作为前沿领域的探索者和实践者&#xff0c;以专业的技术和创新…

下一代电子电气架构(EEA)的关键技术

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…

matlab慕课学习3.5

于20250520 3.5 用while 语句实现循环结构 3.5.1while语句 多用于循环次数不确定的情况&#xff0c;循环次数确定的时候用for更为方便。 3.5.2break语句和continue语句 break用来跳出循环体&#xff0c;结束整个循环。 continue用来结束本次循环&#xff0c;接着执行下一次…

Qt音视频开发过程中一个疑难杂症的解决方法/ffmpeg中采集本地音频设备无法触发超时回调

一、前言 最近在做实时音视频通话的项目中&#xff0c;遇到一个神奇的问题&#xff0c;那就是用ffmpeg采集本地音频设备&#xff0c;当音频设备拔掉后&#xff0c;采集过程会卡死在av_read_frame函数中&#xff0c;尽管设置了超时时间&#xff0c;也设置了超时回调interrupt_c…

PEFT库PromptTuningConfig 配置

PEFT库 PromptTuningConfig 配置 "Prompt Tuning"的参数高效微调 PromptTuningConfig 核心参数解析 1. task_type="CAUSAL_LM" 作用:指定任务类型为因果语言模型(Causal LM)。说明:因果语言模型从左到右生成文本(如GPT系列),这与任务需求匹配(模…

操作系统----软考中级软件工程师(自用学习笔记)

目录 1、计算机系统层次结构 2、程序顺序执行的特征 3、程序并发执行的特征 4、三态模型 5、同步与互斥 6、信号量机制 7、PV操作 8、死锁 9、进程资源图 10、死锁避免 11、线程 12、程序局部性原理 13、分页存储管理 14、单缓冲器 15、双缓冲区 16、磁盘调度算…

基于 Redis 实现短信验证码登录功能的完整方案

&#x1f9f1; 一、技术栈与依赖配置 使用 Spring Boot Redis 实现短信验证码登录&#xff0c;以下是推荐的 Maven 依赖&#xff1a; <dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframework.boot</groupId><ar…

电平匹配电路

1、为什么要电平匹配? 现在很多SOC器件为了降低功耗,都把IO口的电平设计成了1.8V,核电压0.85V,当这种SOC做主平台时,在做接口设计需要格外关注电平的匹配。单板中经常需要将1.8V的电平转换成3.3V或者转成5V。如果没有注意到输入和输出信号之间的电平匹配,系统就无法正常…

JavaScript 日志和调试工具箱-logger2js

原创功能丰富的 JavaScript 日志和调试工具箱&#xff0c;设计这个工具时考虑到了多种实际开发中的需求。该工具不仅提供了高效强大的日志输出显示功能&#xff0c;还包含了界面风格配置、代码格式化、事件处理、性能测试、方法调用栈输出&#xff0c;右键菜单、控制台显示控制…

zData X zStorage 为什么采用全闪存架构而非混闪架构?

点击蓝字 关注我们 最近有用户问到 zData X 的存储底座 zStorage 分布式存储为什么采用的是全闪存架构而非混闪架构&#xff1f;主要原因还是在于全闪存架构在性能和可靠性方面具有更显著的优势。zData X 的上一代产品 zData 的早期版本也使用了SSD盘作为缓存的技术架构&#x…

使用SQLite Studio导出/导入SQL修复损坏的数据库

使用SQLite Studio导出/导入SQL修复损坏的数据库 使用Zotero时遇到了数据库损坏&#xff0c;在软件中寸步难行&#xff0c;遂尝试修复数据库。 一、SQLite Studio简介 SQLite Studio是一款专为SQLite数据库设计的免费开源工具&#xff0c;支持Windows/macOS/Linux。相较于其…