【Linux】————(日志、线程池及死锁问题)

 9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                      作者主页:     作者主页

                                                      本篇博客专栏:Linux

                                                      创作时间 :2024年11月29日

9efbcbc3d25747719da38c01b3fa9b4f.gif

日志

关于日志,首先我们来说一下日志的作用,

作用:

  1. 问题追踪:通过日志不仅仅包括我们程序的一些bug,也可以在安装配置时,通过日志可以发现问题。
  2. 状态监控:通过实时分析日志,可以监控系统的运行状态,做到早发现问题、早处理问题。
  3. 安全审计:审计主要体现在安全上,通过对日志进行分析,可以发现是否存在非授权的操作。

日志并不是越多越详细就越好。在分析运行日志,查找问题时,我们经常遇到该出现的日志没有,无用的日志一大堆,或者有效的日志被大量无意义的日志信息淹没,查找起来非常困难。那么什么时候输出日志呢?以下列出了一些常见的需要输出日志的情况:

常见的需要输出日志的情况:

  • 1. 系统启动参数、环境变量系统启动的参数、配置、环境变量、System.Properties等信息对于软件的正常运行至关重要,这些信息的输出有助于安装配置人员通过日志快速定位问题,所以程序有必要在启动过程中把使用到的关键参数、变量在日志中输出出来。在输出时需要注意,不是一股脑的全部输出,而是将软件运行涉及到的配置信息输出出来。比如,如果软件对jvm的内存参数比较敏感,对最低配置有要求,那么就需要在日志中将-Xms -Xmx -XX:PermSize这几个参数的值输出出来。
  • 2. 异常捕获处在捕获异常处输出日志,大家在基本都能做到,唯一需要注意的是怎么输出一个简单明了的日志信息。这在后面的问题问题中有进一步说明。
  • 3. 函数获得期望之外的结果时一个函数,尤其是供外部系统或远程调用的函数,通常都会有一个期望的结果,但如果内部系统或输出参数发生错误时,函数将无法返回期望的正确结果,此时就需要记录日志,日志的基本通常是warn。需要特别说明的是,这里的期望之外的结果不是说没有返回就不需要记录日志了,也不是说返回false就需要记录日志。比如函数:isXXXXX(),无论返回true、false记录日志都不是必须的,但是如果系统内部无法判断应该返回true还是false时,就需要记录日志,并且日志的级别应该至少是warn。
  • 4. 关键操作关键操作的日志一般是INFO级别,如果数量、频度很高,可以考虑使用DEBUG级别。以下是一些关键操作的举例,实际的关键操作肯定不止这么多。
  • 5.删除:删除一个文件、删除一组重要数据库记录……
  • 5.添加:和外系统交互时,收到了一个文件、收到了一个任务……
  • 7.处理:开始、结束一条任务……

对于日志我们就说这些,下面我们看一下日志的代码:

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <unistd.h>
#include <fstream>
#include <sstream>
#include <filesystem> //c++17
#include <memory>
#include <time.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace LockModule;

    std::string Currtime()
    {
        time_t time_stamp = ::time(nullptr);
        struct tm curr;
        localtime_r(&time_stamp, &curr);

        std::string buffer;
        buffer.resize(100); // 预留足够空间,可根据实际情况调整大小
        std::snprintf(&buffer[0], buffer.size(), "%4d-%02d-%02d %02d:%02d:%02d",
                      curr.tm_year + 1900,
                      curr.tm_mon + 1,
                      curr.tm_mday,
                      curr.tm_hour,
                      curr.tm_min,
                      curr.tm_sec);
        buffer.resize(std::strlen(&buffer[0])); // 调整大小为实际字符串长度
        return buffer;
    }

    // 构成:1.构建日志字符串2.刷新落盘(screen,file)
    // 日志文件的默认路径和文件名

    // 2.日志等级
    enum class LogLevel
    {
        DEBUG = 1,
        INFO, // 正常
        WARNING,
        ERROR,
        FATAL // 致命的
    };
    std::string Level2String(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
            break;
        case LogLevel::ERROR:
            return "ERROR";
            break;
        case LogLevel::FATAL:
            return "FATAL";
            break;
        case LogLevel::INFO:
            return "INFO";
            break;
        case LogLevel::WARNING:
            return "WARNING";
            break;
        default:
            return "";
        }
    }
    const std::string defaultlogpath = "./log/";
    const std::string defaultlogname = "log.txt";
    // 3.刷新策略
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;

    private:
    };

    // 3.1控制台策略
    class ConsoleLogStrategy : public LogStrategy // 继承一下
    {
    public:
        ConsoleLogStrategy()
        {
        }
        ~ConsoleLogStrategy()
        {
        }
        void SyncLog(const std::string &message)
        {
            LockGuard lockguard(_lock); // 保证刷新策略的安全
            std::cout << message << std::endl;
        }

    private:
        Mutex _lock; // 锁
    };

    // 3.2 文件级策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string logpath = defaultlogpath, const std::string logname = defaultlogname)
            : _logpath(logpath),
              _logname(logname)
        {
            LockGuard lockguard(_lock);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }
        ~FileLogStrategy()
        {
        }
        void SyncLog(const std::string &message)
        {
            std::string log = _logpath + _logname;
            std::ofstream out(log, std::ios::app); // 日志写入,一定是追加的
            if (!out.is_open())
            {
                return;
            }
            out << message << "\n";
            out.close();
        }

    private:
        std::string _logpath;
        std::string _logname;
        Mutex _lock;
    };

    // 日志类,构建日志字符串,根据策略进行刷新。
    class Logger
    {
    public:
        Logger()
        {
            // 默认采用ConsoleLogStrategy
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableConsoleLog()
        {
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableFileLog()
        {
            _strategy = std::make_shared<FileLogStrategy>();
        }
        ~Logger()
        {
        }

        // 一条完整的信息:[2024-8-09 12:32:22] [DEBUG]
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
                : _currtime(Currtime()),
                  _level(level),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                std::stringstream ssbuffer;
                ssbuffer << "[" << _currtime << "] "
                         << "[" << Level2String(_level) << "] " << "[" << _pid << "] "
                         << "[" << _filename << "] "
                         << "[" << _line << "] ";
                _loginfo = ssbuffer.str();
            }
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }
            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            std::string _currtime; // 当前日志时间吗,需要可读性,所以不要时间戳
            LogLevel _level;       // 日志等级
            pid_t _pid;            // 进程pid
            std::string _filename; // 源文件名称
            uint32_t _line;        // 日治所在的行号,32位的无符号整数
            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__)
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}

基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性 

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态

但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程

下面我们看一下代码:

RingQueue.hpp:

#pragma once
 
#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <semaphore.h>
 
template <typename T>
class RingQueue
{
private:
    void P(sem_t &s)//申请信号量
    {
        sem_wait(&s);
    }
    void V(sem_t &s)//释放信号量
    {
        sem_post(&s);
    }
public:
    RingQueue(int max_cap)
        : _ringqueue(max_cap), _max_cap(max_cap), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, max_cap);
 
        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    void Push(const T &in) //生产者
    {
        // 信号量:是一个计数器,是资源的预订机制。预订:在外部,可以不判断资源是否满足,就可以知道内部资源的情况!
        P(_space_sem); // 信号量这里,对资源进行使用,申请,为什么不判断一下条件是否满足???信号量本身就是判断条件!
        pthread_mutex_lock(&_p_mutex); //?
        _ringqueue[_p_step] = in;
        _p_step++;
        _p_step %= _max_cap;
        pthread_mutex_unlock(&_p_mutex);
        V(_data_sem);
    }
    void Pop(T *out) // 消费
    {
        P(_data_sem);
        pthread_mutex_lock(&_c_mutex); //?
        *out = _ringqueue[_c_step];
        _c_step++;
        _c_step %= _max_cap;
        pthread_mutex_unlock(&_c_mutex);
        V(_space_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
 
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    std::vector<T> _ringqueue;
    int _max_cap;
 
    int _c_step;
    int _p_step;
 
    sem_t _data_sem; // 消费者关心
    sem_t _space_sem; // 生产者关心
 
    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

Main.cc

#include "RingQueue.hpp"
#include "Task.hpp"
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
 
void *Consumer(void*args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while(true)
    {
        Task t;
        // 1. 消费
        rq->Pop(&t);
 
        // 2. 处理数据
        t();
        std::cout << "Consumer-> " << t.result() << std::endl;
    }
}
void *Productor(void*args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
 
    while(true)
    {
        sleep(1);
 
        // 1. 构造数据
        int x = rand() % 10 + 1; //[1, 10]
        usleep(x*1000);
        int y = rand() % 10 + 1;
        Task t(x, y);
 
        // 2. 生产
        rq->Push(t);
 
        std::cout << "Productor -> " << t.debug() << std::endl;
    }
}
 
int main()
{
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>(5);
    // 单单
    pthread_t c1, c2, p1, p2, p3;
    pthread_create(&c1, nullptr, Consumer, rq);
    pthread_create(&c2, nullptr, Consumer, rq);
    pthread_create(&p1, nullptr, Productor, rq);
    pthread_create(&p2, nullptr, Productor, rq);
    pthread_create(&p3, nullptr, Productor, rq);
 
 
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    return 0;
}

Task.hpp

#pragma once
 
#include<iostream>
#include<functional>
 
// typedef std::function<void()> task_t;
// using task_t = std::function<void()>;
 
// void Download()
// {
//     std::cout << "我是一个下载的任务" << std::endl;
// }
 
 
// 要做加法
class Task
{
public:
    Task()
    {
    }
    Task(int x, int y) : _x(x), _y(y)
    {
    }
    void Excute()
    {
        _result = _x + _y;
    }
    void operator ()()
    {
        Excute();
    }
    std::string debug()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";
        return msg;
    }
    std::string result()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
        return msg;
    }
 
private:
    int _x;
    int _y;
    int _result;
};

 线程池(懒汉单例模式)

 线程池: 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

这里我们主要还是看一下线程池这个代码的实现,其他不在这里展示:
ThreadPool.hpp:

#pragma once
 
#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <functional>
#include "Thread.hpp"
#include "Log.hpp"
#include "LockGuard.hpp"
 
using namespace ThreadMoudle;
using namespace log_ns;
 
static const int gdefaultnum = 5;
 
void test()
{
    while (true)
    {
        std::cout << "hello world" << std::endl;
        sleep(1);
    }
}
 
template <typename T>
class ThreadPool
{
private:
    void LockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void UnlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }
    void Wakeup()
    {
        pthread_cond_signal(&_cond);
    }
    void WakeupAll()
    {
        pthread_cond_broadcast(&_cond);
    }
    void Sleep()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool IsEmpty()
    {
        return _task_queue.empty();
    }
    void HandlerTask(const std::string &name) // this
    {
        while (true)
        {
            // 取任务
            LockQueue();
            while (IsEmpty() && _isrunning)
            {
                _sleep_thread_num++;
                LOG(INFO, "%s thread sleep begin!\n", name.c_str());
                Sleep();
                LOG(INFO, "%s thread wakeup!\n", name.c_str());
                _sleep_thread_num--;
            }
            // 判定一种情况
            if (IsEmpty() && !_isrunning)
            {
                UnlockQueue();
                LOG(INFO, "%s thread quit\n", name.c_str());
                break;
            }
 
            // 有任务
            T t = _task_queue.front();
            _task_queue.pop();
            UnlockQueue();
 
            // 处理任务
            t(); // 处理任务,此处不用/不能在临界区中处理
            // std::cout << name << ": " << t.result() << std::endl;
            LOG(DEBUG, "hander task done, task is : %s\n", t.result().c_str());
        }
    }
    void Init()
    {
        func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);
        for (int i = 0; i < _thread_num; i++)
        {
            std::string threadname = "thread-" + std::to_string(i + 1);
            _threads.emplace_back(threadname, func);
            LOG(DEBUG, "construct thread %s done, init success\n", threadname.c_str());
        }
    }
    void Start()
    {
        _isrunning = true;
        for (auto &thread : _threads)
        {
            LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str());
            thread.Start();
        }
    }
    ThreadPool(int thread_num = gdefaultnum)
        : _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    void operator=(const ThreadPool<T> &) = delete;
 
public:
    void Stop()
    {
        LockQueue();
        _isrunning = false;
        WakeupAll();
        UnlockQueue();
        LOG(INFO, "Thread Pool Stop Success!\n");
    }
 
    // 如果是多线程获取单例呢?
    static ThreadPool<T> *GetInstance()
    {
        if (_tp == nullptr)
        {
            LockGuard lockguard(&_sig_mutex);
            if (_tp == nullptr)
            {
                LOG(INFO, "create threadpool\n");
                // thread-1 thread-2 thread-3....
                _tp = new ThreadPool();
                _tp->Init();
                _tp->Start();
            }
            else
            {
                LOG(INFO, "get threadpool\n");
            }
        }
        return _tp;
    }
 
    void Equeue(const T &in)
    {
        LockQueue();
        if (_isrunning)
        {
            _task_queue.push(in);
            if (_sleep_thread_num > 0)
                Wakeup();
        }
        UnlockQueue();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
 
private:
    int _thread_num;
    std::vector<Thread> _threads;
    std::queue<T> _task_queue;
    bool _isrunning;
 
    int _sleep_thread_num;
 
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
 
    // 单例模式
    // volatile static ThreadPool<T> *_tp;
    static ThreadPool<T> *_tp;
    static pthread_mutex_t _sig_mutex;
};
 
template <typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;

线程安全的单例模式

单例模式是一种 "经典的, 常用的, 常考的" 设计模式

大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式 

单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例. 例如一个男人只能有一个媳妇. 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据.

饿汉实现方式和懒汉实现方式

  • 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
  • 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式. 

 懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度.

 可重入VS线程安全

概念:

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。 

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性 

常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构 

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据 

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。 

可重入与线程安全区别

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

常见锁概念

死锁 

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。 

死锁四个必要条件

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

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

STL,智能指针和线程安全

STL中的容器是否是线程安全的?
不是. 原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响. 而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全

智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题. 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这 个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数. 

其他常见的各种锁 

悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。 

最后:

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。

3.成年人的世界,只筛选,不教育。

4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!

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

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

相关文章

基于深度学习的甲状腺结节影像自动化诊断系统(PyQt5界面+数据集+训练代码)

随着医学影像技术的发展&#xff0c;计算机辅助诊断在甲状腺结节的早期筛查中发挥着重要作用。甲状腺结节的良恶性鉴别对临床治疗具有重要意义&#xff0c;但传统的诊断方法依赖于医生的经验和影像学特征&#xff0c;存在一定的主观性和局限性。为了解决这一问题&#xff0c;本…

本地项目通过git传递给新建的github库

第一步&#xff0c;打开终端进入本地项目目录 第二步&#xff0c;初始化Git仓库 git init第三步&#xff0c;添加远程仓库 git remote add origin https://github.com/用户名/仓库名.git第四步&#xff0c;添加所有文件到Git版本控制 git add .这个命令会将所有文件添加到暂…

【Maven Helper】分析依赖冲突案例

目录 Maven Helper实际案例java文件pom.xml文件运行抛出异常分析 参考资料 《咏鹅》骆宾王 鹅&#xff0c;鹅&#xff0c;鹅&#xff0c;曲项向天歌。 白毛浮绿水&#xff0c;红掌拨清波。 骆宾王是在自己7岁的时候就写下了这首杂言 Maven Helper A must have plugin for wor…

第426场周赛:仅含置位位的最小整数、识别数组中的最大异常值、连接两棵树后最大目标节点数目 Ⅰ、连接两棵树后最大目标节点数目 Ⅱ

Q1、仅含置位位的最小整数 1、题目描述 给你一个正整数 n。 返回 大于等于 n 且二进制表示仅包含 置位 位的 最小 整数 x 。 置位 位指的是二进制表示中值为 1 的位。 2、解题思路 我们需要找到一个整数 x&#xff0c;使得&#xff1a; x ≥ nx 的二进制表示中仅包含置位…

Vue框架开发一个简单的购物车(Vue.js)

让我们利用所学知识来开发一个简单的购物车 &#xff08;记得暴露属性和方法&#xff01;&#xff01;&#xff01;&#xff09; 首先来看一下最基本的一个html框架 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"&…

注册表修改键盘位置

1.winr 输入 regedit 2.HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout 3.右键Keyboard Layout->新建->二进制值->取名Scancode Map 4.右键Scancode Map&#xff0c;修改如下 //第一列 自动生成序号&#xff0c;不用管 第一行 输入8个00 第二…

【Verilog】实验二 数据选择器的设计与vivado集成开发环境

目录 一、实验目的 二、实验环境 三、实验任务 四、实验原理 五、实验步骤 top.v mux2_1.v 一、实验目的 1. 掌握数据选择器的工作原理和逻辑功能。 2. 熟悉vivado集成开发环境。 3. 熟悉vivado中进行开发设计的流程。 二、实验环境 1. 装有vivado的计算机。 2. Sw…

一、文本预处理

文本预处理 前言一、文本处理的基本方法1.1 分词1.1.1 举例&#xff1a;1.1.2 作用1.1.3 分词工具——jieba1.1.3.1 jieba特性1.1.3.2 jieba 的 API 代码演示 二、命名实体识别2.1 命名实体2.2 命名实体识别2.3 举例2.4 作用 三、词性标注3.1 词性3.2 词性标注3.3 jieba词性对照…

【HM-React】02. React基础-下

React表单控制 受控绑定 概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 function App(){const [value, setValue] useState()return (<input type"text" value{value} onChange{e > setValue(e.target.value)}/>) …

二分法篇——于上下边界的扭转压缩间,窥见正解辉映之光(2)

前言 上篇介绍了二分法的相关原理并结合具体题目进行讲解运用&#xff0c;本篇将加大难度&#xff0c;进一步强化对二分法的掌握。 一. 寻找峰值 1.1 题目链接&#xff1a;https://leetcode.cn/problems/find-peak-element/description/ 1.2 题目分析: 题目要求返回数组内…

【C语言】结构体(一)

一&#xff0c;是什么 结构体就是一些值的集合&#xff0c;这些值称为成员变量。 结构体的每个成员可以是不同类型的变量。说到集合&#xff0c;数组也是集合&#xff0c;但是不同的是数组只能是相同类型元素的集合。 二&#xff0c;结构体的声明 struct tag {   member1;…

Leetcode - 周赛425

目录 一&#xff0c;3364. 最小正和子数组 二&#xff0c; 3365. 重排子字符串以形成目标字符串 三&#xff0c;3366. 最小数组和 四&#xff0c;3367. 移除边之后的权重最大和 一&#xff0c;3364. 最小正和子数组 本题可以直接暴力枚举&#xff0c;代码如下&#xff1a; …

uniapp Electron打包生成桌面应用exe文件

1.uniapp Electron打包生成桌面应用exe文件 随着跨平台开发的需求日益增长,UniApp 成为了开发者们的首选之一。通过 UniApp,你可以使用 Vue.js 的语法结构和组件系统来构建原生应用、Web 应用甚至是桌面应用。本文将详细介绍如何使用 UniApp 将你的项目打包成 Windows 桌面端…

Linux -初识 与基础指令1

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【Linux】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 文章目录 &#x1f4da; 前言&#x1f5a5;️ 初识&#x1f510; 登录 root用户&#x1f465; 两种用户➕ 添加用户&#x1f9d1;‍&#x1f4bb; 登录 普通用户⚙️ 常见…

【笔记总结】华为云:应用上云后的安全规划及设计

一、背景和问题 数字化时代&#xff0c;随着信息技术的飞速发展&#xff0c;企业和各类组织纷纷将自身的应用程序迁移至云端。云计算凭借其诸多优势&#xff0c;如成本效益、可扩展性、灵活性以及便捷的资源共享等&#xff0c;已然成为了现代业务运营的重要支撑。 今年&#xf…

【机器学习(二)】分类和回归任务-决策树(Decision Tree,DT)算法-Sentosa_DSML社区版 (1)11111

文章目录 一、算法概念11111二、算法原理&#xff08;一&#xff09;树的构造&#xff08;二&#xff09;划分选择1、信息增益2、基尼指数3、卡方检验 &#xff08;三&#xff09;停止标准&#xff08;四&#xff09;剪枝处理1、预剪枝2、后剪枝 三、决策树的优缺点四、决策树分…

【SpringBoot问题】IDEA中用Service窗口展示所有服务及端口的办法

1、调出Service窗口 打开View→Tool Windows→Service&#xff0c;即可显示。 2、正常情况应该已经出现SpringBoot&#xff0c;如下图请继续第三步 3、配置Service窗口的项目启动类型。微服务一般是Springboot类型。所以这里需要选择一下。 点击最后一个号&#xff0c;点击Ru…

【Linux探索学习】第十八弹——进程等待:深入解析操作系统中的进程等待机制

Linux学习笔记&#xff1a;https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言&#xff1a; 在Linux操作系统中&#xff0c;进程是资源的管理和执行单元&#xff0c;每个进程都有其自己的生命周期。在进程的执行过程中&#xff0c;进程…

如何实现一套键盘鼠标控制两台计算机(罗技Options+ Flow功能快速实现演示)

需求背景 之前我写过一篇文章如何实现一套键盘鼠标控制两台计算机&#xff08;Mouse Without Borders快速上手教程&#xff09;_一套键鼠控制两台电脑-CSDN博客 当我们在局域网内有两台计算机&#xff0c;想使用一套键鼠操控时&#xff0c;可以安装Mouse Without Borders软件…

MATLAB中物理系统建模仿真介绍

Simscape 是 MATLAB 提供的一个强大的多域物理系统建模和仿真工具&#xff0c;它允许用户在 Simulink 环境中创建和模拟复杂的物理系统。Simscape 模型可以帮助用户细化需求、设计控制系统、测试嵌入式控制器&#xff0c;并使用数字孪生支持在役运行 1. Simscape概述 Simscap…