当前位置: 首页 > article >正文

Linux -日志 | 线程池 | 线程安全 | 死锁

文章目录

    • 1.日志
      • 1.1日志介绍
      • 1.2策略模式
      • 1.3实现日志类
    • 2.线程池
      • 2.1线程池介绍
      • 2.2线程池的应用场景
      • 2.3线程池的设计
      • 2.4代码实现
      • 2.5修改为单例模式
    • 3.线程安全和函数重入问题
      • 3.1线程安全和函数重入的概念
      • 3.2总结
    • 4.死锁
      • 4.1什么是死锁
      • 4.2产生死锁的必要条件
      • 4.3避免死锁


1.日志

1.1日志介绍

计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信
息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯ 具。
⽇志格式以下⼏个指标是必须得有的
• 时间戳
• 日志等级
• 日志内容
以下几个指标是可选的 • 文件名行号 • 进程,线程相关id信息等。

1.2策略模式

1.2.1介绍

策略模式(Strategy Pattern)是一种对象行为型设计模式,它定义了一系列算法,并将每个算法封装在独立的类中,使得它们可以相互替换。这样,算法的变化就不会影响到使用算法的客户端代码,从而提高了代码的灵活性和可维护性。

1.2.2策略模式的结构和角色

策略模式通常由以下几个角色组成:

  1. 抽象策略类(Strategy):

    • 定义了一个公共接口,用于封装具体的算法。

    • 不同的具体策略类会实现这个接口,提供不同的算法实现。

  2. 具体策略类(Concrete Strategy):

    • 实现了抽象策略类定义的算法接口,具体实现了具体的算法逻辑。

    • 每个具体策略类都代表了一种具体的算法或策略。

  3. 上下文类(Context):

    • 持有一个策略类的引用,在客户端代码中通过该引用调用具体策略的方法。

    • 上下文类还可以维护一些公共的状态或行为,这些状态或行为可以在不同的策略之间共享。

1.2.3策略模式的优点

  1. 提高了代码的灵活性和可维护性:

    • 由于算法被封装在独立的策略类中,因此可以方便地添加、删除或修改算法,而不需要修改客户端代码。

    • 这使得系统更加易于维护和扩展。

  2. 遵循了开闭原则:

    • 策略模式允许在不修改现有代码的情况下添加新的策略类,从而实现了对扩展的开放和对修改的关闭。

  3. 减少了条件语句的使用:

    • 在不使用策略模式的情况下,客户端代码中可能会包含大量的条件语句来根据不同的算法进行选择。

    • 而使用策略模式后,这些条件语句可以被封装在策略类中,客户端只需要选择合适的策略类进行调用即可。

  4. 实现了算法的定义、选择和使用的分离:

    • 策略模式将算法的定义、选择和使用分离开来,使得算法可以独立变化,而不影响使用算法的客户端代码。

1.2.4策略模式的缺点

  1. 增加了系统中的类和对象数量:

    • 由于每个具体策略类都需要一个单独的类进行实现,因此这可能会增加系统的复杂性。

  2. 客户端需要了解不同的策略类:

    • 客户端需要了解并选择合适的策略类进行调用,这可能会增加客户端的复杂性。

  3. 可能导致客户端代码变得复杂

1.3实现日志类

1.3.1实现的格式

[具体时间] [日志的类型] [进程id] [源文件名][行号] 其他内容 

1.3.2策略

采用策略模拟实现,实现输出到屏幕和输出到文件两种策略。

1.3.3代码实现

//Mutex.hpp -- 互斥锁

#pragma once
#include <iostream>
#include <pthread.h>

namespace LockModule
{
    class Mutex
    {
        Mutex(const Mutex &) = delete;
        const Mutex &operator=(const Mutex &) = delete;

    public:
        Mutex()
        {
            int n = pthread_mutex_init(&_lock, nullptr); // 初始化锁
            (void)n;                                     // 防止报警告
        }

        // 上锁
        void Lock()
        {
            int n = pthread_mutex_lock(&_lock);
            (void)n;
        }

        // 返回锁指针
        pthread_mutex_t *LockPtr()
        {
            return &_lock;
        }

        // 解锁
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_lock);
            (void)n;
        }

        ~Mutex()
        {
            int n = pthread_mutex_destroy(&_lock);
            (void)n;
        }

    private:
        pthread_mutex_t _lock;
    };

    //二次封装 -- 这样可以不用解锁了
    class LockGuard
    {
    public:
        LockGuard(Mutex &mtx):_mtx(mtx)
        {
            _mtx.Lock();
        }
        ~LockGuard()
        {
            _mtx.Unlock();
        }
    private:
        Mutex &_mtx;
    };

}

/
//log.hpp --日志

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"

namespace LogMudule
{
    using namespace LockModule;

    // 获取时间 格式 年-月-日 时-分-秒
    std::string CurrentTime()
    {
        // 获取时间搓
        time_t _time = time(nullptr);

        // 转化为具体时间
        struct tm curr;
        localtime_r(&_time, &curr);

        // 转化为字符串
        char buff[1024];
        snprintf(buff, sizeof(buff), "%d-%d-%d %d-%d-%d",
                 curr.tm_year + 1900, // 从1900开始算
                 curr.tm_mon + 1,     // 从0开始算
                 curr.tm_mday,
                 curr.tm_hour,
                 curr.tm_min,
                 curr.tm_sec);

        return buff;
    }

    // 日志文件的默认路径和文件名
    const std::string defaultlogpath = "./";
    const std::string defaultlogname = "log.txt";

    // 日志等级
    enum class LogLevel
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 映射日志等级
    std::string Level2String(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNIGN";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            break;
        }

        //不存在
        return "None";
    }

    // 刷新策略.
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0; // 纯虚函数
    };

    // 输出到屏幕策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {}
        ~ConsoleLogStrategy()
        {}
        void SyncLog(const std::string &message)    //message - 日志内容
        {
            // 加锁 -- 防止乱序
            LockGuard _lock(_mutex);
            std::cout << message << std::endl;
        }

    private:
        Mutex _mutex;
    };

    // 文件级(磁盘)策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)
            : _logpath(logpath),
              _logname(logname)
        {
            // 加锁 -- 线程安全
            LockGuard _lock(_mutex);

            // 当前目录存在
            if (std::filesystem::exists(_logpath))
            {
                return;
            }

            // 不存在则创建目录
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }

        // 向磁盘文件输出
        void SyncLog(const std::string &message) //message - 日志内容
        {
            // 加锁 -- 防止乱序
            LockGuard _lock(_mutex);

            // 以最加式打开文件
            std::string log = _logpath + _logname;
            std::ofstream _ofs(log, std::ios::app);

            if (!_ofs.is_open())
            {
                return;
            }

            _ofs << message << '\n';

            _ofs.close();
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logname;

        // 锁
        Mutex _mutex;
    };

    // 日志类 - 根据刷新策略进行刷新
    class Logger
    {
    public:
        Logger()
        {
            // 默认向屏幕刷新
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }

        // 设置向屏幕输出
        void EnableConsoleLog()
        {
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }

        // 设置向文件输出
        void EnableFileLog()
        {
            _strategy = std::make_shared<FileLogStrategy>();
        }

        ~Logger() {}

        //内部类
        class LogMessage
        {
        public:
            //初始化成员变量
            LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
                : _currtime(CurrentTime()),
                  _level(level),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 将数据构成一条完整的字符串
                std::stringstream ssbuff;
                ssbuff << "[" << _currtime << "]"
                       << "[" << Level2String(_level) << "]"
                       << "[" << _pid << "]"
                       << "[" << _filename << "]"
                       << "[" << _line << "]";

                _loginfo = ssbuff.str();
            }

            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // 添加字符串内容
                std::stringstream s;
                s << info;
                _loginfo += s.str();

                return *this;
            }

            //在析构时调用
            ~LogMessage()
            {
                if (_logger._strategy != nullptr)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            std::string _currtime; // 当前日志的时间
            LogLevel _level;       // 日志等级
            pid_t _pid;            // 进程pid
            std::string _filename; // 源文件名称
            int _line;             // 日志所在的行号
            Logger &_logger;       // 负责根据不同的策略进行刷新
            std::string _loginfo;  // 一条完整的日志记录
        };

        // 就是要拷贝,故意的拷贝-- 这样执行完语句自动销毁就会调用析构实行策略
        LogMessage operator()(LogLevel level, const std::string &filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

    private:
        std::shared_ptr<LogStrategy> _strategy; // 日志刷新的策略方案
    };

//实例
Logger logger;

//使用宏替换
#define LOG(Level) logger(Level, __FILE__, __LINE__)    // 调用重载()  __FILE__, __LINE__ -- 获取源文件名和当前行号

//切换策略
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()  
#define ENABLE_FILE_LOG() logger.EnableFileLog()

}



//Main.cc
#include"log.hpp"

using namespace LogMudule;

int main()                                              
{
   	ENABLE_CONSOLE_LOG()
    LOG(LogLevel::DEBUG)<<"我是其他内容";
    return 0;
}                           

执行效果
在这里插入图片描述

2.线程池

2.1线程池介绍

⼀种线程使⽤模式。线程过多会带来调度开销,进⽽影响缓存局部性和整体性能。⽽线程池维护着多 个线程,等待着监督管理者分配可并发执⾏的任务。这避免了在处理短时间任务时创建与销毁线程的 代价。线程池不仅能够保证内核的充分利⽤,还能防⽌过分调度。可⽤线程数量应该取决于可⽤的并发处理器、处理器内核、内存、⽹络sockets等的数量。

2.2线程池的应用场景

  1. 需要⼤量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应⽤,⽐如要求服务器迅速响应客⼾请求。
  3. 接受突发性的⼤量请求,但不⾄于使服务器因此产⽣⼤量线程的应⽤。

2.3线程池的设计

创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执⾏任务对象中的任务接⼝。

在这里插入图片描述

2.4代码实现


//条件变量封装
//cond.hpp 
#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

namespace CondModule
{
    using namespace LockModule;

    class Cond
    {
    public:
        Cond()
        {
            int n = pthread_cond_init(&_cond, nullptr); // 初始化条件变量
            (void)n;
        }

        // 等待
        void Wait(Mutex &mutex) // 让我们的线程释放曾经持有的锁!Mutex -- 封装的锁
        {
            int n = pthread_cond_wait(&_cond, mutex.LockPtr()); // 进行等待
            (void)n;
        }

        // 唤醒至少一个线程
        void Notify()
        {
            int n = pthread_cond_signal(&_cond);
            (void)n;
        }

        // 唤醒全部线程
        void NotifyAll()
        {
            int n = pthread_cond_broadcast(&_cond);
            (void)n;
        }

        ~Cond()
        {
            int n = pthread_cond_destroy(&_cond);
            (void)n;
        }

    private:
        pthread_cond_t _cond; // 条件变量
    };
}



//Thread.hpp -- 创建线程封装
#ifndef _THREAD_HPP__
#define _THREAD_HPP__

#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>

namespace ThreadModule
{
    //函数包装
    using func_t = std::function<void(std::string)>;
    //线程个数
    static int number = 1;
    //状态
    enum class TSTATUS
    {
        NEW,
        RUNNING,
        STOP
    };

    class Thread
    {
    private:
        //执行线程函数
        static void *Routine(void *args)
        {
            //强制类型转换
            Thread* thread = (Thread*)args;

            std::cout<<"我是"<<thread->_name<<"  :";
            //调用任务
            thread->_func(thread->_name);
            
            return nullptr;
        }

    public:
        Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(false)
        {
            _name = "Thread-" + std::to_string(number);
            _pid = getpid();
            number++;
        }

        //创建线程
        bool Start()
        {
            if (_status != TSTATUS::RUNNING) // 保证线程处于非运行状态
            {
                int n = pthread_create(&_tid, nullptr, Routine, (void *)this); // 创建线程
                if (n != 0)
                {
                    return false;
                }
                _status = TSTATUS::RUNNING; // 更新状态
                return true;
            }

            return false;
        }

        //取消线程
        bool Stop()
        {
            if(_status == TSTATUS::RUNNING) //保证线程处于运行状态
            {
                int n = pthread_cancel(_tid);   //取消线程
                if(n != 0)
                {
                    return false;
                }
                _status = TSTATUS::STOP; // 更新状态
                return true;
            }

            return false;
        }

        //等待线程
        bool Join()
        {
            //保证线程不处于分离状态且处于运行状态
            if(!_joinable && _status == TSTATUS::RUNNING)  
            {   
                //等待线程,默认不关心线程状态
                int n = pthread_join(_tid,nullptr);
                if(n != 0)
                {
                    return false;
                }
                _status = TSTATUS::STOP; // 更新状态
                return true;
            }

            return false;
        }

        //线程分离
        void Detach()
        {
            //保证线程不处于分离状态且处于运行状态
            if(!_joinable && _status == TSTATUS::RUNNING)  
            {
               int n = pthread_detach(_tid);    //进行线程分离
               if(n != 0)
               {
                    return ;
               }
               std::cout<<"完成线程分离\n"<<std::endl;
               _joinable = true; //更新分离状态
            }
        }

        bool IsJoinable()
        {
            return _joinable;
        }

        std::string Name()
        {
            return _name;
        }

        ~Thread() {}

    private:
        std::string _name; // 线程name
        pthread_t _tid;    // 线程id
        pid_t _pid;        // 进程id
        bool _joinable;    // 是否是分离的,默认不是
        func_t _func;      // 线程执行的任务
        TSTATUS _status;   // 线程状态
    };
}

#endif
/
//ThreadPool.hpp  线程池
namespace ThreadPoolModule
{
    //展开命名空间
    using namespace LogMudule;
    using namespace ThreadModule;
    using namespace LockModule;
    using namespace CondModule;

    //重命名
    using thread_t = std::shared_ptr<Thread>;

    //线程池类
    template <typename T>
    class ThreadPool
    {
    private: 
        const static int defaultnum = 5; //默认的线程数量 
        //注意:要加static,不然defaultnum需要实例化才生成,这会导致编译时失败

        //判断任务队列是否为空
        bool IsEmpty()
        {
            return _taskq.empty();
        }

        //线程同一执行的函数
        void HandlerTask(std::string name)
        {
            //日志
            LOG(LogLevel::DEBUG)<<name<<"进入主逻辑";

            //1.拿任务
            while(true)
            {
                T t;    //任务

                LockGuard l(_lock); //访问临界资源 - 加互斥锁


                //为空且为运行状态时才进行等待
                while(IsEmpty() && _isrunning)
                {
                    LOG(LogLevel::DEBUG)<<name<<"进入等待";
                    _wait_num++;
                    _cond.Wait(_lock);
                    _wait_num--;
                    LOG(LogLevel::DEBUG)<<name<<"完成等待";
                } 

                //走到这里只有任务队列不为空或者_isrunning == false -- 这种情况需要结束线程
                if(IsEmpty() && !_isrunning)
                {
                    break;
                }

                //2.取任务
                t = _taskq.front();
                _taskq.pop();

                 //3.执行任务
                t(name);
            }
        }

    public:
        
        //初始化成员变量
        ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false)
        {
            for(int i = 0; i < _num; i++)
            {
                //初始化线程池 - 此时还未正式创建线程
                _threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1)));
                LOG(LogLevel::DEBUG)<<"对象"<<i<<"完成创建";
            }
        }

        //进入任务队列
        void Equeue(T &&in)
        {
            //线程池不在运行状态
            if(_isrunning == false)
                return;

            访问临界资源 - 加互斥锁
            LockGuard l(_lock);
            _taskq.push(std::move(in));

            //当有等待线程时需要唤醒
            if(_wait_num > 0)
            {
                _cond.Notify();
                LOG(LogLevel::DEBUG)<<"唤醒一个线程";
            }
        }

        //启动线程池 -- 正式创建线程
        void Start()
        {
            //已经是运行的状态了
            if(_isrunning)
                return ;

            //第一次设置为运行状态
            _isrunning = true;

            for(auto &e : _threads)
            {
                e->Start();
            }
        }

        //进程线程等待
        void Wait()
        {
            for(auto &e : _threads)
            {
                e->Join();
            } 
        }

        //暂停线程池
        void Stop()
        {
            访问临界资源 - 加互斥锁
            LockGuard l(_lock);
            if(_isrunning == false)
                return;

            //修改状态  -- 此时不能添加任务了
            _isrunning = false;

            //唤醒其他线程,再让其他线程将剩下的任务执行完了,最后再自己退出。
            if(_wait_num > 0)
            {
                _cond.NotifyAll();
            }
        }
        ~ThreadPool()
        {}

    private:
        std::vector<thread_t> _threads; //线程组s
        int _num;   //线程数量
        int _wait_num;  //正在等待的线程数量
        std::queue<T> _taskq; // 临界资源
        Mutex _lock;    //锁
        Cond _cond;     //条件变量
        bool _isrunning;    //当前线程状态
    };

}
//
//Main.cc
#include"ThreadPool.hpp"


using namespace ThreadPoolModule;

void add(std::string name)
{
    LOG(LogLevel::DEBUG)<<name<<"进入计算";
}

int main()
{
    ThreadPool<func_t> tp;
    //启动
    tp.Start();

    int n = 10;
    while(n--)
    {
        //入任务
        tp.Equeue(add);
        sleep(1);
    }

    //暂停
    tp.Stop();
    //等待
    tp.Wait();
    return 0;
}

执行效果
在这里插入图片描述

2.5修改为单例模式

2.5.1什么是单例模式

该类的实例化对象在整个代码中只能有一个。

2.5.2实现单例模式的方式

  1. 饿汉模式:不管什么,一上来先创建好。
  2. 懒汉模式:需要用时才进行创建。

2.5.3使用懒汉模式实现线程池单例

//1。将构造函数私有化和删除拷贝构造和赋值重载 -这样外部就无法创建了
private:
  ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false)
        {
            for(int i = 0; i < _num; i++)
            {
                _threads.push_back(std::make_shared<Thread>		(std::bind(&ThreadPool::HandlerTask,this,std::placeholders::_1)));
                LOG(LogLevel::DEBUG)<<"对象"<<i<<"完成创建";
            }
        }

        ThreadPool(const ThreadPool<T> & tp) = delete;
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
		
		//2.提供一个静态线程池对象指针和一个互斥锁
		static ThreadPool<T> *tp;   //线程池指针
        static Mutex _mutex;    //为单例的锁
        
    public:
    //3.提供静态函数-全局可用,需要用线程池时就调用获取线程池对象
        static ThreadPool<T> *getInstance()
        {
        	//双if判断可以对减少对锁的申请
            if(tp == nullptr)
            {
            	//因为被多线程调用时,当多个线程同时进入时可能会被创建多个,所以加互斥锁
                LockGuard l(_mutex);
                if(tp == nullptr)
                {	
                	//创建线程池
                    tp = new ThreadPool<T>();
                    LOG(LogLevel::DEBUG)<<"第一次完成创建";
                }
            }
            return tp;
        }
/// 
//调用方式
//main.cc
void add(std::string name)
{
    LOG(LogLevel::DEBUG)<<name<<"进入计算";
}
int main()
{
    ThreadPool<func_t>::getInstance()->Start();


    int n = 10;
    while(n--)
    {
        ThreadPool<func_t>::getInstance()->Equeue(add);
        sleep(1);
    }


    ThreadPool<func_t>::getInstance()->Stop();
    ThreadPool<func_t>::getInstance()->Wait();
    return 0;
}

3.线程安全和函数重入问题

3.1线程安全和函数重入的概念

  1. 线程安全就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执行结果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
  2. 重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊,我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被 称为可重⼊函数,否则,是不可重⼊函数。
    一般的可重入:
    多线程访问函数
    信号导致一个执行流多次进入同一个函数

3.2总结

可重入函数一定是线程安全的,线程安全的函数不一定是可重入的(如一个执行流正在执行一个加了锁的线程安全函数并且此时拿着锁,但是由于信号(该信号的处理函数也是当前函数)导致该线程中断并去执行信号处理函数,当该执行流申请锁时被锁阻塞,而该执行流又无法回去释放锁,这使当前线程一直被阻塞,从而出现死锁)。
补充:信号是由进程其中的一个线程处理的,每个线程中都有信号屏蔽字,信号屏蔽字可以设置屏蔽信号。

4.死锁

4.1什么是死锁

死锁是指在⼀组进程或者线程中的各个进程或者线程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的⼀种永久等待状态。如:假设临界资源同时需要两把锁才能访问,线程A申请了其中一把锁,线程B申请了另一把锁,此时线程A和B都会去申请没拿到的锁,但是线程A和B都不释放自己手里的锁,这导致线程A和B都被一直阻塞住了。

4.2产生死锁的必要条件

  1. 互斥条件:⼀个资源每次只能被⼀个执行流使用。
  2. 请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若⼲执行流之间形成⼀种头尾相接的循环等待资源的关系。

4.3避免死锁

破坏其中的一个条件即可。
如:破坏循环等待条件问题:资源⼀次性分配, 使⽤超时机制、加锁顺序⼀致


http://www.kler.cn/a/410943.html

相关文章:

  • 【设计模式】【行为型模式(Behavioral Patterns)】之状态模式(State Pattern)
  • 代码美学:MATLAB制作渐变色
  • 使用ENSP实现默认路由
  • .net 8使用hangfire实现库存同步任务
  • QML学习 —— 29、3种不同使用动画的方式(附源码)
  • BERT的中文问答系统42
  • 自由学习记录(25)
  • 充满智慧的埃塞俄比亚狼
  • 图像分割——区域增长
  • AIGC--AIGC与人机协作:新的创作模式
  • ffmpeg RTP PS推流
  • ESC字符背后的故事(27 <> 033 | x1B ?)
  • 高性能 ArkUI 应用开发:复杂 UI 场景中的内存管理与 XML 优化
  • Android OpenGL ES详解——绘制圆角矩形
  • CGAL CGAL::Polygon_mesh_processing::self_intersections解析
  • AIGC与SEO:如何提升网站流量
  • 从零开始理解JVM:对象的生命周期之对象销毁(垃圾回收)
  • python学习笔记(12)算法(5)迭代与递归
  • 搜维尔科技:TechViz中的手指追踪:触摸并与虚拟物体互动
  • 排序算法2
  • 基于SSM的宠物领养平台
  • 【前端学习笔记】AJAX、axios、fetch、跨域
  • c++中数组的特点,vector容器的实现(增删改查各个接口的实现)
  • 【DVWA】File Inclusion文件包含实战
  • 快速理解微服务中Sentinel怎么实现限流
  • elasticsearch的索引管理