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避免死锁

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【博主推荐】C#的winfrom应用中datagridview常见问题及解决方案汇总

文章目录 1.datagridview绘制出现鼠标悬浮数据变空白2.datagridview在每列前动态添加序号2.1 加载数据集完成后绘制序号2.2 RowPostPaint事件绘制 3.datagridview改变行样式4.datagridview后台修改指定列数据5.datagridview固定某个列宽6.datagridview某个列的显示隐藏7.datagr…

【设计模式】创建型模式之单例模式(饿汉式 懒汉式 Golang实现)

定义 一个类只允许创建一个对象或实例&#xff0c;而且自行实例化并向整个系统提供该实例&#xff0c;这个类就是一个单例类&#xff0c;它提供全局访问的方法。这种设计模式叫单例设计模式&#xff0c;简称单例模式。 单例模式的要点&#xff1a; 某个类只能有一个实例必须…

Vivado程序固化到Flash

在上板调试FPGA时&#xff0c;通常使用JTAG接口下载程序到FPGA芯片中&#xff0c;FPGA本身是基于RAM工艺的器件&#xff0c;因此掉电后会丢失芯片内的程序&#xff0c;需要重新烧写程序。但是当程序需要投入使用时不能每一次都使用JTAG接口下载程序&#xff0c;一般FPGA的外围会…

技术文档,they are my collection!

工作 今天这篇文章&#xff0c;献给一直撰写技术文档的自己。我自认为是公司中最爱写文档的人了&#xff0c;我们是一个不到40人的小公司&#xff0c;公司作风没有多么严谨&#xff0c;领导也不会要求我们写技术文档。但是从入职初至今&#xff0c;我一直保持着写技术文档…

微信小程序学习指南从入门到精通

&#x1f5fd;微信小程序学习指南从入门到精通&#x1f5fd; &#x1f51d;微信小程序学习指南从入门到精通&#x1f51d;✍前言✍&#x1f4bb;微信小程序学习指南前言&#x1f4bb;一、&#x1f680;文章列表&#x1f680;二、&#x1f52f;教程文章的好处&#x1f52f;1. ✅…

JavaWeb——SpringBoot原理

10.1. 配置优先级 10.1.1. 配置文件 properties > yml(推荐) > yaml 10.1.2. Java系统属性、命令行参数 命令行参数 > Java系统属性 > 配置文件 10.2. Bean管理 10.2.1. 手动获取bean ApplicationContext&#xff0c;IOC容器对象 10.2.2. bean作用域 10.2.3.…

如何在Python中进行数学建模?

数学建模是数据科学中使用的强大工具&#xff0c;通过数学方程和算法来表示真实世界的系统和现象。Python拥有丰富的库生态系统&#xff0c;为开发和实现数学模型提供了一个很好的平台。本文将指导您完成Python中的数学建模过程&#xff0c;重点关注数据科学中的应用。 数学建…

OCR技术详解:从基础到应用

OCR技术详解&#xff1a;从基础到应用 引言 OCR技术的定义 OCR&#xff08;Optical Character Recognition&#xff0c;光学字符识别&#xff09;是一种将印刷或手写文本转换为机器可读文本的技术。通过OCR技术&#xff0c;计算机可以自动识别图像中的文字&#xff0c;并将其…

webrtc视频会议学习(三)

文章目录 关联&#xff1a;源码搭建coturn服务器nginx配置ice配置需服务器要开放的端口 效果 关联&#xff1a; webrtcP2P音视频通话&#xff08;一&#xff09; webrtcP2P音视频通话&#xff08;二&#xff09; webrtc视频会议学习&#xff08;三&#xff09; 源码 WebRTC…

【从零开始的LeetCode-算法】43. 网络延迟时间

有 n 个网络节点&#xff0c;标记为 1 到 n。 给你一个列表 times&#xff0c;表示信号经过 有向 边的传递时间。 times[i] (ui, vi, wi)&#xff0c;其中 ui 是源节点&#xff0c;vi 是目标节点&#xff0c; wi 是一个信号从源节点传递到目标节点的时间。 现在&#xff0c;…

数据结构--AVL树(平衡二叉树)

✅博客主页:爆打维c-CSDN博客​​​​​​ &#x1f43e; &#x1f539;分享c、c知识及代码 &#x1f43e; &#x1f539;Gitee代码仓库 五彩斑斓黑1 (colorful-black-1) - Gitee.com 一、AVL树是什么&#xff1f;&#xff08;含义、性质&#xff09; 1.AVL树的概念 AVL树是最…

sunshine和moonlight串流网络丢失帧高的问题(局域网)

注&#xff1a;此贴结果仅供参考 场景环境&#xff1a;单身公寓 路由器&#xff1a;2016年的路由器 开始&#xff1a;电脑安装sunshine软件&#xff0c;手机安装moonlight软件开始串流发现网络丢失帧发现巨高 一开始怀疑就是路由器问题&#xff0c;因为是局域网&#xff0c;而…

STM32F103外部中断配置

一、外部中断 在上一节我们介绍了STM32f103的嵌套向量中断控制器&#xff0c;其中包括中断的使能、失能、中断优先级分组以及中断优先级配置等内容。 1.1 外部中断/事件控制器 在STM32f103支持的60个可屏蔽中断中&#xff0c;有一些比较特殊的中断&#xff1a; 中断编号13 EXTI…

解决SSL VPN客户端一直提示无法连接服务器的问题

近期服务器更新VPN后&#xff0c;我的win10电脑一致无法连接到VPN服务器&#xff0c; SSL VPN客户端总是提示无法连接到服务端。网上百度尝试了各种方法后&#xff0c;终于通过以下设置方式解决了问题&#xff1a; 1、首先&#xff0c;在控制面板中打开“网络和共享中心”窗口&…

从零开始:Linux 环境下的 C/C++ 编译教程

个人主页&#xff1a;chian-ocean 文章专栏 前言&#xff1a; GCC&#xff08;GNU Compiler Collection&#xff09;是一个功能强大的编译器集合&#xff0c;支持多种语言&#xff0c;包括 C 和 C。其中 gcc 用于 C 语言编译&#xff0c;g 专用于 C 编译。 Linux GCC or G的安…

小程序-基于java+SpringBoot+Vue的网上花店微信小程序设计与实现

项目运行 1.运行环境&#xff1a;最好是java jdk 1.8&#xff0c;我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境&#xff1a;IDEA&#xff0c;Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境&#xff1a;Tomcat 7.x,8.x,9.x版本均可 4.硬件环境&#xff1a…

Transformer:一种革命性的序列到序列学习框架

目录 ​编辑 引言 Transformer模型的基本结构 1. 自注意力机制 2. 前馈神经网络 3. 位置编码 Transformer的工作原理 Transformer的应用 机器翻译 文本摘要 问答系统 文本分类 语音识别 图像识别 结论 引言 Transformer模型&#xff0c;自2017年由Vaswani等人提…

轮转数组(java)

题目描述 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,…

【vue3实现微信小程序】每日专题与分页跳转的初步实现

快速跳转&#xff1a; 我的个人博客主页&#x1f449;&#xff1a;Reuuse博客 新开专栏&#x1f449;&#xff1a;Vue3专栏 参考文献&#x1f449;&#xff1a;uniapp官网 免费图标&#x1f449;&#xff1a;阿里巴巴矢量图标库 ❀ 感谢支持&#xff01;☀ 前情提要 &#x…

【优先算法学习】双指针--结合题目讲解学习

目录 1.有效三角形的个数 1.2题目解题思路 1.3代码实现 2.和为s的两个数 2.1刷题链接-> 2.2题目解题思路 2.3代码实现 1.有效三角形的个数 1.1刷题链接-> 力扣-有效三角形的个数https://leetcode.cn/problems/valid-triangle-number/description/ 1.2题目解…