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

【Linux】线程池、单例模式、死锁

线程池

  • 一.线程池
    • 1.日志和策略模式
    • 2.线程池
      • 1.Task.hpp
      • 2.Thread.hpp
      • 3.ThreadPool.hpp
      • 4.ThreadPool.cc
  • 二.线程安全与重入问题
  • 三.线程安全的单例模式
    • 1.饿汉模式
    • 2.懒汉模式
    • 3.懒汉模式线程池
      • 1.ThreadPool.hpp
      • 2.ThreadPool.cc
  • 四.死锁的概念
    • 1.死锁
    • 2.死锁的四个必要条件
    • 3.避免死锁
  • 五.STL、智能指针和线程安全?

本节重点:

  • 设计日志和线程池。
  • 理解线程安全和可重入,掌握锁相关概念。

一.线程池

在写线程池之前,我们要做如下准备:

  • 准备线程的封装。
  • 准备锁和条件变量的封装。
  • 引入日志,对线程进行封装。

1.日志和策略模式

  • 日志:记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
  • 日志格式中的某些指标是必须有:时间戳、日志等级、日志内容。存在几个指标是可选的:文件名行号、进程,线程相关id信息等。
  • 日志有现成的解决方案:spdlog、glog、Boost.Log、Log4cxx等。日志位于 /var/log/ 路径下
  • 设计模式:在软件开发过程中,针对反复出现的问题所总结归纳出的通用解决方案。

策略模式:

  • 抽象策略类(基类):包含一个或多个纯虚函数,用于声明具体策略类需要实现的接口。
  • 具体策略类(派生类):重写了抽象策略类中定义的接口,每个具体策略类代表一个具体的接口。
  • 上下文类:持有一个抽象策略类的指针/引用,负责根据需要选择和使用具体的策略类。

抽象策略类的作用:定义统一接口,运行时多态,提高代码的可维护性和可扩展性。

这里采用 设计模式 - 策略模式 来进行日志的设计,我们想要的日志格式如下:

[可读性很好的时间] [日志等级] [进程pid] [打印对应日志的文件名][行号] - 消息内容, 支持可变参数[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [9] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [10] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [11] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [12] - hello world
// Log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> // C++17文件系统
#include <fstream>    // 文件流
#include <sstream>    // 字符串流
#include <memory>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;// 获取系统时间std::string CurrentTime(){time_t time_stamp = ::time(nullptr); // 获取时间戳struct tm curr;localtime_r(&time_stamp, &curr); // 将时间戳转化为可读性强的信息char buffer[1024];snprintf(buffer, sizeof(buffer), "%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);return buffer;}// 日志文件: 默认路径和默认文件名const std::string defaultlogpath = "./log/";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 "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "NONE";}}// 3. 策略模式: 刷新策略class LogStrategy{public:virtual ~LogStrategy() = default; //???// 纯虚函数: 无法实例化对象, 派生类可以重载该函数, 实现不同的刷新方式virtual void SyncLog(const std::string &message) = 0;};// 3.1 控制台策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy() {}~ConsoleLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << std::endl;}private:Mutex _mutex;};// 3.2 文件级(磁盘)策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname): _logpath(logpath), _logname(logname){// 判断_logpath目录是否存在if (std::filesystem::exists(_logpath)){return;}try{std::filesystem::create_directories(_logpath);}catch (std::filesystem::filesystem_error &e){std::cerr << e.what() << "\n";}}~FileLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string log = _logpath + _logname;std::ofstream out(log, std::ios::app); // 以追加的方式打开文件if (!out.is_open()){return;}out << message << "\n"; // 将信息刷新到out流中out.close();}private:std::string _logpath;std::string _logname;Mutex _mutex;};// 4. 日志类: 构建日志字符串, 根据策略进行刷新class Logger{public:Logger(){// 默认往控制台上刷新_strategy = std::make_shared<ConsoleLogStrategy>();}~Logger() {}void EnableConsoleLog(){_strategy = std::make_shared<ConsoleLogStrategy>();}void EnableFileLog(){_strategy = std::make_shared<FileLogStrategy>();}// 内部类: 记录完整的日志信息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 ssbuffer;ssbuffer << "[" << _currtime << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "] - ";_loginfo = ssbuffer.str();}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}template <class T>LogMessage &operator<<(const T &info){std::stringstream ssbuffer;ssbuffer << info;_loginfo += ssbuffer.str();return *this;}private:std::string _currtime;  // 当前日志时间LogLevel _level;       // 日志水平pid_t _pid;            // 进程pidstd::string _filename; // 文件名uint32_t _line;        // 日志行号Logger &_logger;       // 负责根据不同的策略进行刷新std::string _loginfo;  // 日志信息};// 故意拷贝, 形成LogMessage临时对象, 后续在被<<时,会被持续引用,// 直到完成输入,才会自动析构临时LogMessage, 至此完成了日志的刷新,// 同时形成的临时对象内包含独立日志数据, 未来采用宏替换, 获取文件名和代码行数LogMessage operator()(LogLevel level, const std::string &filename, int line){return LogMessage(level, filename, line, *this);}private:// 纯虚类不能实例化对象, 但是可以定义指针std::shared_ptr<LogStrategy> _strategy; // 日志刷新策略方案};// 定义全局logger对象Logger logger;// 编译时进行宏替换: 方便随时获取行号和文件名
#define LOG(level) logger(level, __FILE__, __LINE__)// 提供选择使用何种日志策略的方法
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}// Main.cc
#include <iostream>
#include "Log.hpp"
using namespace LogModule;int main()
{// 往显示器中写入ENABLE_CONSOLE_LOG();LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";// 往文件中写入ENABLE_FILE_LOG();LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";LOG(LogLevel::DEBUG) << "hello world";return 0;
}
xzy@hcss-ecs-b3aa:~$ ./testLog 
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [9] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [10] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [11] - hello world
[2025-03-08 00:43:30] [DEBUG] [882217] [Main.cc] [12] - hello world

2.线程池

  • 线程池:创建一定数量线程,这些线程处于等待任务的状态。如果没有任务,线程在条件变量下等待,直到有任务到来。当有新的任务到来时,线程池会唤醒一个线程执行任务。当任务执行完毕后,线程不会被销毁,而是返回到线程池中等待下一个任务,从而实现线程的复用。

线程池使用场景:

  • 高并发场景:在处理大量并发请求的场景中,如 Web 服务器、数据库服务器等,使用线程池可以有效地处理并发请求,提高系统的吞吐量。
  • 任务执行频繁的场景:当程序中需要频繁地执行一些小任务时,使用线程池可以避免频繁地创建和销毁线程,提高程序的效率。
  • 需要控制线程数量的场景:在一些对系统资源有限制的场景中,如嵌入式系统、移动设备等,使用线程池可以控制线程的数量,避免系统资源耗尽。

这里我们实现:创建固定数量线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口。

在这里插入图片描述

1.Task.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"using namespace LogModule;void MySql(std::string name)
{LOG(LogLevel::DEBUG) << "我是一个数据任务, 我正在被执行" << "[" << name << "]";
}void UpLoad(std::string name)
{LOG(LogLevel::DEBUG) << "我是一个上传任务, 我正在被执行" << "[" << name << "]";
}void DownLoad(std::string name)
{LOG(LogLevel::DEBUG) << "我是一个下载任务, 我正在被执行" << "[" << name << "]";
}using task_t = std::function<void(std::string name)>;
std::vector<task_t> tasks;

2.Thread.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#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表示不需要this指针, 否则回调函数报错// 而要执行_func()函数又需要由this指针, 所以Routine函数传this指针static void *Routine(void *args){Thread *t = static_cast<Thread *>(args);t->_func(t->Name());return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func): _func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}~Thread() {}// 线程创建bool Start(){if (_status != TSTATUS::RUNNING){int n = pthread_create(&_tid, nullptr, Routine, 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){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}// 线程分离bool Detach(){EnableDetach();int n = ::pthread_detach(_tid);if (n != 0)return false;return true;}// 线程是否分离bool IsJoinable() {  return _joinable; }std::string Name() { return _name; }private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 线程是否是分离的, 默认不是func_t _func;TSTATUS _status;};
}

3.ThreadPool.hpp

#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <memory>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Log.hpp"namespace ThreadPoolModule
{using namespace MutexModule;using namespace CondModule;using namespace ThreadModule;using namespace LogModule;using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 5;template <class T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 进入HandlerTask执行逻辑";while (true){// 1. 拿任务: 访问共享资源, 需要加锁T task;{LockGuard lockguard(_mutex);while (IsEmpty() && _isrunning) // while替代if: 防止伪唤醒{_wait_num++;_cond.Wait(_mutex); // 没任务时: 线程在条件变量上阻塞等待_wait_num--;}// 2. 任务队列不为空 && 线程池退出if (IsEmpty() && !_isrunning)break;task = _taskq.front();_taskq.pop();}// 3. 处理任务: 并发处理, 不需要持有锁task(name);}LOG(LogLevel::INFO) << "线程: " << name << ", 退出";}public:ThreadPool(int num = defaultnum): _num(num), _wait_num(0), _isrunning(false){for (int i = 0; i < _num; i++){// 在类中: bind类的公有方法, 需要取地址 + 传入this指针// 在类外: bind类的公有方法, 需要取地址 + 传入类的匿名对象_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1))); // push_back()会调用移动构造LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象...成功";}}~ThreadPool() {}void Equeue(const T &in){LockGuard lockguard(_mutex);if (!_isrunning) return;_taskq.push(in);if (_wait_num > 0){_cond.Signal(); // 唤醒线程}}void Start(){if (_isrunning) return;_isrunning = true;for (auto &thread_ptr : _threads){thread_ptr->Start();LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "...成功";}}void Stop(){LockGuard lockguard(_mutex);if (_isrunning){// 1. 不能再新增任务了_isrunning = false;// 2. 让线程自己退出(唤醒所有的线程) && 历史任务被执行完if (_wait_num > 0){_cond.Broadcast();}}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "...成功";}}private:int _num;                       // 线程的个数std::vector<thread_t> _threads; // 线程池std::queue<T> _taskq;           // 共享资源: 任务队列int _wait_num;                  // 等待的线程数目bool _isrunning;                // 线程池是否运行Mutex _mutex; // 锁Cond _cond;   // 条件变量};
}

4.ThreadPool.cc

#include <iostream>
#include <memory>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Task.hpp"using namespace ThreadPoolModule;int main()
{ENABLE_CONSOLE_LOG();tasks.push_back(MySql);tasks.push_back(UpLoad);tasks.push_back(DownLoad);std::shared_ptr<ThreadPool<task_t>> tp = std::make_shared<ThreadPool<task_t>>(3);tp->Start();int cnt = 0;while(cnt < 6){tp->Equeue(tasks[cnt % 3]);cnt++;sleep(1);}tp->Stop();sleep(3);tp->Wait();return 0;
}
xzy@hcss-ecs-b3aa:~$ ./thread_pool 
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [67] - 构建线程Thread-1对象...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [67] - 构建线程Thread-2对象...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [67] - 构建线程Thread-3对象...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [91] - 启动线程Thread-1...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [91] - 启动线程Thread-2...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [91] - 启动线程Thread-3...成功
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [31] - 线程: Thread-1, 进入HandlerTask执行逻辑
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [31] - 线程: Thread-2, 进入HandlerTask执行逻辑
[2025-03-11 17:32:01] [INFO] [1030242] [ThreadPool.hpp] [31] - 线程: Thread-3, 进入HandlerTask执行逻辑
[2025-03-11 17:32:01] [DEBUG] [1030242] [Task.hpp] [12] - 我是一个数据任务, 我正在被执行[Thread-1]
[2025-03-11 17:32:02] [DEBUG] [1030242] [Task.hpp] [17] - 我是一个上传任务, 我正在被执行[Thread-2]
[2025-03-11 17:32:03] [DEBUG] [1030242] [Task.hpp] [22] - 我是一个下载任务, 我正在被执行[Thread-3]
[2025-03-11 17:32:04] [DEBUG] [1030242] [Task.hpp] [12] - 我是一个数据任务, 我正在被执行[Thread-1]
[2025-03-11 17:32:05] [DEBUG] [1030242] [Task.hpp] [17] - 我是一个上传任务, 我正在被执行[Thread-2]
[2025-03-11 17:32:06] [DEBUG] [1030242] [Task.hpp] [22] - 我是一个下载任务, 我正在被执行[Thread-3]
[2025-03-11 17:32:07] [INFO] [1030242] [ThreadPool.hpp] [55] - 线程: Thread-2, 退出
[2025-03-11 17:32:07] [INFO] [1030242] [ThreadPool.hpp] [55] - 线程: Thread-3, 退出
[2025-03-11 17:32:07] [INFO] [1030242] [ThreadPool.hpp] [55] - 线程: Thread-1, 退出
[2025-03-11 17:32:10] [INFO] [1030242] [ThreadPool.hpp] [116] - 回收线程Thread-1...成功
[2025-03-11 17:32:10] [INFO] [1030242] [ThreadPool.hpp] [116] - 回收线程Thread-2...成功
[2025-03-11 17:32:10] [INFO] [1030242] [ThreadPool.hpp] [116] - 回收线程Thread-3...成功

二.线程安全与重入问题

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

重入的两种情况:

  • 多线程重入函数。
  • 信号导致一个执行流重复进入函数。

注意:在多进程中,可能会发生重入,但是访问共享资源会发生写时拷贝,不会出现问题!

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三.线程安全的单例模式

  • 单例模式:只能创建一个对象!
  • 在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百G)到内存中。此时往往要用一个单例的类来管理这些数据。

1.饿汉模式

  • 初始化时机:在程序启动时,类被加载到内存后就立即初始化单例对象,无论后续是否会使用到该对象。
  • 线程安全性:由于是在程序启动时就完成初始化,在多线程环境下,不存在多个线程同时创建单例对象的问题,所以通常是线程安全的。
// 饿汉模式存在问题
// 1. 多个饿汉模式的单例,某个对象初始化内容较多(读文件),会导致程序启动慢
// 2. A和B两个饿汉,对象初始化存在依赖关系,要求A先初始化,B再初始化,无法保证
class InfoMar
{
public:static InfoMar* GetInstance(){return &_ins;}private:InfoMar(){cout << "ip:" << _ip << endl;cout << "port: " << _port << endl;}// 禁用: 拷贝构造和拷贝赋值InfoMar(const InfoMar&) = delete;InfoMar& operator=(const InfoMar&) = delete;private:string _ip = "127.0.0.1";int _port = 8080;// 静态对象与普通对象不同: 不在类中, 而是在静态区, 类域的限制static InfoMar _ins;
};// 静态成员: 类内声明, 类外定义
InfoMar InfoMar::_ins;int main()
{// 程序运行在该行: 静态对象就被创建好了return 0;
}

2.懒汉模式

  • 初始化时机:单例对象在第一次被使用时才进行初始化,在未被使用之前,不会创建对象。
  • 线程安全性:如果不做额外的线程安全处理,在多线程环境下,当多个线程同时访问获取单例对象的方法,且此时单例对象尚未初始化时,就可能会导致多个线程同时创建单例对象,破坏单例模式的唯一性。因此,懒汉模式需要通过加锁等机制来保证线程安全。
// 懒汉模式: 解决饿汉模式的两个问题
class InfoMar
{
public:static InfoMar* GetInstance(){// 若单例为空: 需要加锁创建单例对象if (_pins == nullptr){mutex.lock(); if (_pins == nullptr){_pins = new InfoMar;}mutex.unlock();}// 若单例不为空: 直接返回单例对象return _pins;}static void DelInstance(){delete _pins;_pins = nullptr;}private:InfoMar(){cout << "ip:" << _ip << endl;cout << "port: " << _port << endl;}// 禁用: 拷贝构造和拷贝赋值InfoMar(const InfoMar&) = delete;InfoMar& operator=(const InfoMar&) = delete;private:string _ip = "127.0.0.1";int _port = 8080;static InfoMar* _pins; // 单例对象static Mutex mutex;    // 保护单例对象
};InfoMar* InfoMar::_pins = nullptr;
Mutex InfoMar::mutex;int main()
{InfoMar::GetInstance();return 0;
}

3.懒汉模式线程池

1.ThreadPool.hpp

#pragma once#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <memory>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Log.hpp"namespace ThreadPoolModule
{using namespace MutexModule;using namespace CondModule;using namespace ThreadModule;using namespace LogModule;using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 3;template <class T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 进入HandlerTask执行逻辑";while (true){// 1. 拿任务: 访问共享资源, 需要加锁T task;{LockGuard lockguard(_mutex);while (IsEmpty() && _isrunning) // while替代if: 防止伪唤醒{_wait_num++;_cond.Wait(_mutex); // 没任务时: 线程在条件变量上阻塞等待_wait_num--;}// 2. 任务队列不为空 && 线程池退出if (IsEmpty() && !_isrunning)break;task = _taskq.front();_taskq.pop();}// 3. 处理任务: 并发处理, 不需要持有锁task(name);}LOG(LogLevel::INFO) << "线程: " << name << ", 退出";}ThreadPool(int num = defaultnum): _num(num), _wait_num(0), _isrunning(false){for (int i = 0; i < _num; i++){// 在类中: bind类的公有方法, 需要取地址 + 传入this指针// 在类外: bind类的公有方法, 需要取地址 + 传入类的匿名对象_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1))); // push_back()会调用移动构造LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象...成功";}}ThreadPool<T>(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;public:~ThreadPool() {}// 获取单例对象static ThreadPool<T> *GetInstance(){// 若单例为空: 需要加锁创建单例对象if(instance == nullptr){LockGuard lockguard(_lock);if(instance == nullptr){LOG(LogLevel::INFO) << "单例首次被执行, 需要加载对象...";instance = new ThreadPool<T>();}}// 若单例不为空: 直接返回单例对象return instance;}void Equeue(const T &in){LockGuard lockguard(_mutex);if (!_isrunning) return;_taskq.push(in);if (_wait_num > 0){_cond.Signal(); // 唤醒线程}}void Start(){if (_isrunning) return;_isrunning = true;for (auto &thread_ptr : _threads){thread_ptr->Start();LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << "...成功";}}void Stop(){LockGuard lockguard(_mutex);if (_isrunning){// 1. 不能再新增任务了_isrunning = false;// 2. 让线程自己退出(唤醒所有的线程) && 历史任务被执行完if (_wait_num > 0){_cond.Broadcast();}}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << "...成功";}}private:int _num;                       // 线程的个数std::vector<thread_t> _threads; // 线程池std::queue<T> _taskq;           // 共享资源: 任务队列int _wait_num;                  // 等待的线程数目bool _isrunning;                // 线程池是否运行Mutex _mutex; // 锁Cond _cond;   // 条件变量static ThreadPool<T> *instance; // 单例对象static Mutex _lock;             // 用来保护单例};// 静态成员: 类内声明, 类外定义template<class T>ThreadPool<T> *ThreadPool<T>::instance = nullptr;template<class T>Mutex ThreadPool<T>::_lock;
}

2.ThreadPool.cc

#include <iostream>
#include <memory>
#include <unistd.h>
#include "ThreadPool.hpp"
#include "Task.hpp"using namespace ThreadPoolModule;int main()
{ENABLE_CONSOLE_LOG();tasks.push_back(MySql);tasks.push_back(UpLoad);tasks.push_back(DownLoad);ThreadPool<task_t>::GetInstance()->Start();int cnt = 0;while(cnt < 6){ThreadPool<task_t>::GetInstance()->Equeue(tasks[cnt % 3]);cnt++;sleep(1);}ThreadPool<task_t>::GetInstance()->Stop();sleep(3);ThreadPool<task_t>::GetInstance()->Wait();return 0;
}
xzy@hcss-ecs-b3aa:~$ ./thread_pool 
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [85] - 单例首次被执行, 需要加载对象...
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [66] - 构建线程Thread-1对象...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [66] - 构建线程Thread-2对象...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [66] - 构建线程Thread-3对象...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [111] - 启动线程Thread-1...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [111] - 启动线程Thread-2...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [111] - 启动线程Thread-3...成功
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [31] - 线程: Thread-3, 进入HandlerTask执行逻辑
[2025-03-12 20:46:40] [DEBUG] [1077712] [Task.hpp] [12] - 我是一个数据任务, 我正在被执行[Thread-3]
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [31] - 线程: Thread-2, 进入HandlerTask执行逻辑
[2025-03-12 20:46:40] [INFO] [1077712] [ThreadPool.hpp] [31] - 线程: Thread-1, 进入HandlerTask执行逻辑
[2025-03-12 20:46:41] [DEBUG] [1077712] [Task.hpp] [17] - 我是一个上传任务, 我正在被执行[Thread-3]
[2025-03-12 20:46:42] [DEBUG] [1077712] [Task.hpp] [22] - 我是一个下载任务, 我正在被执行[Thread-2]
[2025-03-12 20:46:43] [DEBUG] [1077712] [Task.hpp] [12] - 我是一个数据任务, 我正在被执行[Thread-1]
[2025-03-12 20:46:44] [DEBUG] [1077712] [Task.hpp] [17] - 我是一个上传任务, 我正在被执行[Thread-3]
[2025-03-12 20:46:45] [DEBUG] [1077712] [Task.hpp] [22] - 我是一个下载任务, 我正在被执行[Thread-2]
[2025-03-12 20:46:46] [INFO] [1077712] [ThreadPool.hpp] [55] - 线程: Thread-1, 退出
[2025-03-12 20:46:46] [INFO] [1077712] [ThreadPool.hpp] [55] - 线程: Thread-3, 退出
[2025-03-12 20:46:46] [INFO] [1077712] [ThreadPool.hpp] [55] - 线程: Thread-2, 退出
[2025-03-12 20:46:49] [INFO] [1077712] [ThreadPool.hpp] [136] - 回收线程Thread-1...成功
[2025-03-12 20:46:49] [INFO] [1077712] [ThreadPool.hpp] [136] - 回收线程Thread-2...成功
[2025-03-12 20:46:49] [INFO] [1077712] [ThreadPool.hpp] [136] - 回收线程Thread-3...成功

四.死锁的概念

1.死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
  • 为了发便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问。

在这里插入图片描述

申请一把锁是原子的,但是申请两把锁就不一定了:

在这里插入图片描述

造成的结果是:

在这里插入图片描述

2.死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。

在这里插入图片描述

  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。

在这里插入图片描述

  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

在这里插入图片描述

3.避免死锁

破坏死锁的四个必要条件
◦ 破坏循环等待条件问题:资源⼀次性分配,使⽤超时机制、加锁顺序⼀致

五.STL、智能指针和线程安全?

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

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

  • 对于 unique_ptr,不支持拷贝,只支持移动,所以它不会在多个线程之间共享,也就不存在多个线程同时访问导致数据竞争等线程安全问题。
  • 对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
  • 注意:这里的线程安全是指智能指针的接口是线程安全的,但是智能指针指向的对象未必是线程安全的!

相关文章:

【Linux】线程池、单例模式、死锁

线程池 一.线程池1.日志和策略模式2.线程池1.Task.hpp2.Thread.hpp3.ThreadPool.hpp4.ThreadPool.cc 二.线程安全与重入问题三.线程安全的单例模式1.饿汉模式2.懒汉模式3.懒汉模式线程池1.ThreadPool.hpp2.ThreadPool.cc 四.死锁的概念1.死锁2.死锁的四个必要条件3.避免死锁 五…...

AI+视频监控电力巡检:EasyCVR视频中台方案如何赋能电力行业智能化转型

随着电力行业的快速发展&#xff0c;电力设施的安全性、稳定性和运维效率变得至关重要。传统视频监控系统在实时性、智能化及多系统协同等方面面临严峻挑战。EasyCVR视频中台解决方案作为一种先进的技术手段&#xff0c;在电力行业中得到了广泛应用&#xff0c;为电力设施的监控…...

centos8.0系统部署zabbix6.0监控

centos8.0系统部署zabbix6.0监控 一、部署过程1、确认系统版本2、主机基础环境设置3、安装MySQL 8.0数据库3.1 安装MySQL 8.0仓库3.2 安装软件3.3 设置root用户密码3.4 创建zabbix数据库&#xff0c;授权用户 4、配置zabbix6.0仓库5、安装zabbix服务端软件6、导入zabbix数据表7…...

江科大51单片机笔记【12】AT24C02(I2C总线)

写在前言 此为博主自学江科大51单片机&#xff08;B站&#xff09;的笔记&#xff0c;方便后续重温知识 在后面的章节中&#xff0c;为了防止篇幅过长和易于查找&#xff0c;我把一个小节分成两部分来发&#xff0c;上章节主要是关于本节课的硬件介绍、电路图、原理图等理论知识…...

电脑一直重启怎么解决 原因及解决方法

电脑一直重启的故障状态&#xff0c;不仅影响电脑的正常使用&#xff0c;还可能导致数据丢失或损坏。那么&#xff0c;电脑一直重启是什么原因呢&#xff1f;又该如何解决呢&#xff1f;下面将为大家介绍电脑一直重启的常见原因和解决方法&#xff0c;帮助您恢复电脑的正常工作…...

内网安全防护新思路 —— HFish + ELK 与 T-Pot 全面蜜罐系统比较分析

在当前网络安全环境日益复杂的背景下&#xff0c;企业和组织面临着来自外部与内部的多种威胁。为了更好地了解攻击者行为、捕获恶意活动并及时响应&#xff0c;部署蜜罐&#xff08;Honeypot&#xff09;系统已成为提升内网安全防护的重要手段。本文将重点介绍两种内网蜜罐防护…...

「 机器人 」扑翼飞行器通过总气动力控制四自由度运动方法

一、前言 在扑翼飞行中,总气动力(Total Aerodynamic Force)是指扑翼在运动过程中受到的所有空气动力作用的合力。它是由以下两种主要力的合成结果: 1. 升力(Lift, ):垂直于空气流方向的力,用于支持飞行器(或生物)的重量。 2. 阻力(Drag, ):平行于空气流方向的力,…...

写了一个二叉树构造函数和画图函数,方便debug

代码 class TreeNode(object):def __init__(self, val, leftNone, rightNone):self.val valself.left leftself.right rightdef construct_tree(nodes):if not nodes:return Noneroot TreeNode(nodes[0])queue [root]index 1while index < len(nodes):node queue.p…...

【prompt实战】知乎问题解答专家

本文原创作者&#xff1a;姚瑞南 AI-agent 大模型运营专家&#xff0c;先后任职于美团、猎聘等中大厂AI训练专家和智能运营专家岗&#xff1b;多年人工智能行业智能产品运营及大模型落地经验&#xff0c;拥有AI外呼方向国家专利与PMP项目管理证书。&#xff08;转载需经授权&am…...

元组(Tuple)详解——c#

在C#中&#xff0c;元组&#xff08;Tuple&#xff09; 是一种轻量级的数据结构&#xff0c;用于将多个值组合成一个单一的对象。元组非常适合在不需要定义新类或结构体的情况下&#xff0c;临时存储和传递多个相关的值。 C# 中的元组有两种形式&#xff1a; 传统元组&#xf…...

Maven工具基础知识(一)

第一章、Maven概述 一、概述 官网地址&#xff1a;Welcome to Apache Maven – Maven Maven是一个基于Java的项目管理工具&#xff0c;专注于项目构建、依赖管理和项目信息标准化。其核心目标 是简化开发流程&#xff0c;通过标准化项目结构和自动化构建流程&#xff…...

AI模型的构建过程是怎样的(下)

你好,我是舒旻。 上节课,我们讲了一个模型构建的前 2 个环节,模型设计和特征工程。今天,我们继续来讲模型构建的其他 3 个环节,说说模型训练、模型验证和模型融合中,算法工程师的具体工作内容,以及 AI 产品经理需要掌握的重点。 模型训练 模型训练是通过不断训练、验证…...

力扣hot100_二叉树

二叉树的建立与遍历 #include <iostream> #include <vector> #include <queue> using namespace std;// 定义二叉树节点 struct TreeNode {int val;TreeNode* left;TreeNode* right;TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} };// 函数&…...

如何制作Windows系统盘、启动盘?(MediaCreationTool_22H2)

文章目录 每日一句正能量前言一、准备工作二、制作启动盘后记 每日一句正能量 每个在你生命里出现的人&#xff0c;都有原因。喜欢你的人给你温暖关心。你喜欢的人让你学会爱和付出&#xff0c;不喜欢你的人让你自省成长。你不喜欢的人教会你宽容尊重&#xff0c;没有人是偶然出…...

分布式光伏发电的发展现状与前景

分布式光伏发电的发展现状与前景 1、分布式光伏发电的背景2、分布式光伏发电的分类2.1、集中式光伏发电2.1.1、特点、原则2.1.2、优点2.1.3、缺点 2.2、分布式光伏发电2.2.1、特点、原则2.2.2、优点2.2.3、缺点 2.3、对比 3、分布式光伏发电的现状4、分布式光伏发电的应用场景4…...

【AI大模型智能应用】Deepseek生成测试用例

在软件开发过程中&#xff0c;测试用例的设计和编写是确保软件质量的关键。 然而&#xff0c;软件系统的复杂性不断增加&#xff0c;手动编写测试用例的工作量变得异常庞大&#xff0c;且容易出错。 DeepSeek基于人工智能和机器学习&#xff0c;它能够依据软件的需求和设计文…...

NVIDIA k8s-device-plugin源码分析与安装部署

在《kubernetes Device Plugin原理与源码分析》一文中&#xff0c;我们从源码层面了解了kubelet侧关于device plugin逻辑的实现逻辑&#xff0c;本文以nvidia管理GPU的开源github项目k8s-device-plugin为例&#xff0c;来看看设备插件侧的实现示例。 一、Kubernetes Device Pl…...

面向联邦学习隐私保护的同态加密库优化算法研究

面向联邦学习隐私保护的同态加密库优化算法研究 一、引言 联邦学习作为一种新兴的分布式机器学习范式,允许各参与方在不共享原始数据的前提下协同训练模型,有效解决了数据孤岛和隐私保护问题。同态加密作为实现联邦学习隐私保护的关键技术之一,能够在密文上直接进行特定运算…...

20250212:linux系统DNS解析卡顿5秒的bug

问题: 1:人脸离线识别记录可以正常上传云端 2:人脸在线识别请求却一直超时 3:客户使用在线网络 思路:...

动态规划 -第1篇

前言&#xff1a;在计算机科学中&#xff0c;动态规划&#xff08;Dynamic Programming&#xff0c;简称DP&#xff09;是解决最优化问题的一种重要方法。通过将大问题拆解为小问题&#xff0c;动态规划不仅能够显著降低计算复杂度&#xff0c;还能提高效率。无论是经典的背包问…...

uni-app打包成H5使用相对路径

网上找了一圈&#xff0c;没用&#xff0c;各种试&#xff0c;终于给试出来了&#xff0c;导致打包之后请求的路径没有带上域名 直接去 config.js文件里面的baseUrl路径改成空字符就行了&#xff0c;千万别写/...

【每日学点HarmonyOS Next知识】swiper样式、日期选择、自定义弹窗键盘、文本组件换行、富文本适配

1、HarmonyOS swiper组件样式&#xff1f; 可在样式中设置即可&#xff0c;参考如下demo Entry Component struct SwiperDemo {private swiperController: SwiperController new SwiperController();build() {Column({ space: 5 }) {Swiper(this.swiperController) {Text(0)…...

STM32 I2C驱动开发全解析:从理论到实战 | 零基础入门STM32第五十步

主题内容教学目的/扩展视频I2C总线电路原理&#xff0c;跳线设置&#xff0c;I2C协议分析。驱动程序与调用。熟悉I2C总线协议&#xff0c;熟练调用。 师从洋桃电子&#xff0c;杜洋老师 &#x1f4d1;文章目录 引言一、I2C驱动分层架构二、I2C总线驱动代码精析2.1 初始化配置&a…...

Ragflow技术栈分析及二次开发指南

Ragflow是目前团队化部署大模型+RAG的优质方案,不过其仍不适合直接部署使用,本文将从实际使用的角度,对其进行二次开发。 1. Ragflow 存在问题 Ragflow 开源仓库地址:https://github.com/infiniflow/ragflow Ragflow 当前版本: v0.17.0 Ragflow 目前主要存在以下问题: …...

力扣hot100二刷——链表

第二次刷题不在idea写代码&#xff0c;而是直接在leetcode网站上写&#xff0c;“逼”自己掌握常用的函数。 标志掌握程度解释办法⭐Fully 完全掌握看到题目就有思路&#xff0c;编程也很流利⭐⭐Basically 基本掌握需要稍作思考&#xff0c;或者看到提示方法后能解答⭐⭐⭐Sl…...

【Godot4.3】斜抛运动轨迹曲线点求取函数

概述 原文写于去年9月。一篇测试性的文章。 基于初始位置和初始速度的抛物线 # 抛物运动轨迹曲线 - 基于初始位置和初始速度计算 func projectile_motion_curve(start_pos:Vector2, # 物体的起始位置velocity:Vector2, # 初始速度nums:int, …...

SpringBoot基础Kafka示例

这里将生产者和消费者放在一个应用中 使用的Boot3.4.3 引入Kafka依赖 <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId> </dependency>yml配置 spring:application:name: kafka-1#kafka…...

【安卓逆向】安卓病毒介绍及其简单案例分析

目录 引言 一、Android 病毒介绍及分析方法 1.1 Android 病毒预览 1.2 Android 病毒分析必备知识 1.3 Android 病毒的常见类型及恶意行为 1.3.1 常见病毒类型 1.3.2 常见病毒行为 1.4 病毒激活条件 1.5 Android 病毒的传播方式 1.6 Android 病毒分析的一般方法 二…...

Git的命令学习——适用小白版

浅要了解一下Git是什么&#xff1a; Git是目前世界上最先进的的分布式控制系统。Git 和其他版本控制系统的主要差别在于&#xff0c;Git 只关心文件数据的整体是否发生变化&#xff0c;而大多数其他系统则只关心文件内容的具体差异。Git 并不保存这些前后变化的差异数据。实际上…...

Bad owner or permissions on ssh/config - 解决方案

问题 在Windows系统通过ssh连接远程服务器时报错&#xff1a; ssh [ssh_user][ip] Bad owner or permissions on C:\\Users\\[win_user]/.ssh/config原因 这是因为.ssh文件夹或.ssh/config文件的权限异常&#xff0c;当前Windows账号没有读写权限导致的。 Windows系统重装&a…...

AI 赋能软件开发:从工具到思维的全面升级

喜欢可以到主页订阅专栏 引言 在当今技术飞速发展的时代,人工智能(AI)正以前所未有的速度渗透到各个领域,软件开发行业也不例外。AI 不仅改变了开发工具的使用方式,更深刻地影响了开发者的思维模式和工作流程。从代码生成到错误检测,从性能优化到项目管理,AI 正在全面…...

【Ubuntu系统设置固定内网ip,且不影响访问外网 】

Ubuntu系统安装后&#xff0c;由于每次重新开机会被重新分配内网ip&#xff0c;所以我们可以设置固定内网ip&#xff0c;且不影响访问外网&#xff0c;亲测有效 打开【终端】&#xff0c;查看当前内网ip&#xff08;inet&#xff09;&#xff0c;子网掩码&#xff08;netmask&a…...

VSCode集成C语言开发环境

下载MinGW https://sourceforge.net/projects/mingw/ 点击download按钮下载exe文件到本地 点击exe文件安装 选择基础包和c编译版 vscode安装部分跳过 安装code runner和c/c插件 **(1) 创建 C 文件** 新建一个测试文件&#xff08;例如 hello.c&#xff09;&#xf…...

力扣:3305.元音辅音字符串计数

给你一个字符串 word 和一个 非负 整数 k。 返回 word 的 子字符串 中&#xff0c;每个元音字母&#xff08;a、e、i、o、u&#xff09;至少 出现一次&#xff0c;并且 恰好 包含 k 个辅音字母的子字符串的总数。 示例 1&#xff1a; 输入&#xff1a;word "aeioqq"…...

vscode接入DeepSeek 免费送2000 万 Tokens 解决DeepSeek无法充值问题

1. 在vscode中安装插件 Cline 2.打开硅基流动官网 3. 注册并登陆&#xff0c;邀请码 WpcqcXMs 4.登录后新建秘钥 5. 在vscode中配置cline (1) API Provider 选择 OpenAI Compatible &#xff1b; (2) Base URL设置为 https://api.siliconflow.cn](https://api.siliconfl…...

【ELK】ElasticSearch 集群常用管理API操作

目录 常用_cat 概览 集群状态 集群配置 集群磁盘使用 索引信息与配置 shard - 分片 查看段信息 nodes -节点 用户与权限 tasks 和 pending_tasks allocation - 均衡 thread_pool -线程 templete 模版 ILM 生命周期 其他 集群版本&#xff1a; 7.17.6 API地址&…...

BambuStudio学习笔记:MTUtils

# MTUtils.hpp 功能解析## 文件概述 该头文件提供了多线程同步工具和数值生成功能&#xff0c;主要包含以下组件&#xff1a;### 核心组件1. **自旋锁 (SpinMutex)**- 基于原子操作的高性能锁- 实现Lockable概念&#xff0c;可与标准库锁守卫配合使用2. **缓存对象模板 (Cached…...

DeepSeek开启AI办公新模式,WPS/Office集成DeepSeek-R1本地大模型!

从央视到地方媒体&#xff0c;已有多家媒体机构推出AI主播&#xff0c;最近杭州文化广播电视集团的《杭州新闻联播》节目&#xff0c;使用AI主持人进行新闻播报&#xff0c;且做到了0失误率&#xff0c;可见AI正在逐渐取代部分行业和一些重复性的工作&#xff0c;这一现象引发很…...

Ubuntu 22.04 安装配置 FTP服务器 教程

今天搞定在 Ubuntu 22.04 系统上安装和配置 VSFTPD &#xff0c;还会涉及防火墙设置、SSL/TLS 设置&#xff0c;以及创建专门登录 FTP 服务器的账户。开始&#xff01; 一、安装 VSFTPD 首先&#xff0c;咱得让系统知道有啥新软件包可以安装。打开终端&#xff0c;输入下面这…...

2021-05-28 C++自己写一个strcmp函数

规则 当s1<s2时&#xff0c;返回为负数-1&#xff1b; 当s1s2时&#xff0c;返回值 0&#xff1b; 当s1>s2时&#xff0c;返回正数1。 即&#xff1a;两个字符串自左向右逐个字符相比&#xff08;按ASCII值大小相比较&#xff09;&#xff0c;直到出现不同的字符或遇…...

版本控制器Git(3)

文章目录 前言一、分支管理策略二、Bug分支管理遇到Bug时的处理方法使用 git stash 暂存工作区内容创建并切换到Bug修复分支恢复之前的工作 三、临时分支的删除总结 前言 我们在上篇讲到了分支&#xff0c;现在我们就着这个继续来讲解&#xff01; 一、分支管理策略 master分支…...

TDengine 使用教程:从入门到实践

TDengine 是一款专为物联网&#xff08;IoT&#xff09;和大数据实时分析设计的时序数据库。它能够高效地处理海量的时序数据&#xff0c;并提供低延迟、高吞吐量的性能表现。在本文中&#xff0c;我们将带领大家从 TDengine 的安装、基本操作到一些高级功能&#xff0c;帮助你…...

Python Web项目的服务器部署

一.部署运行 1.虚拟环境的安装&#xff1a;&#xff08;一行一行运行&#xff09; wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh bash miniconda.sh -b -p /opt/miniconda3 echo export PATH"/opt/miniconda3/bin:$PAT…...

linux - ubuntu 使用时一些小问题整理 --- 持续更新

目录 ubuntu 中 root用户默认不存在解决办法 在Ubuntu系统中&#xff0c;将用户添加到sudoers文件中&#xff0c;使其能够以超级用户权限执行命令&#xff0c;通常可以通过以下几种方法实现&#xff1a; 方法1&#xff1a;将用户添加到sudo组 方法2&#xff1a;直接编辑sud…...

道路运输安全员考试:备考中的心理调适与策略

备考道路运输安全员考试&#xff0c;心理调适同样重要。考试压力往往会影响考生的学习效率和考试发挥。​ 首先&#xff0c;要正确认识考试压力。适度的压力可以激发学习动力&#xff0c;但过度的压力则会适得其反。当感到压力过大时&#xff0c;要学会自我调节。可以通过运动…...

关于WPS的Excel点击单元格打开别的文档的两种方法的探究【为单元格添加超链接】

问题需求 目录和文件结构如下&#xff1a; E:\Dir_Level1 │ Level1.txt │ └─Dir_Level2│ Level2.txt│ master.xlsx│└─Dir_Level3Level3.txt现在要在master.xlsx点击单元格进而访问Level1.txt、Level2.txt、Level3.txt这些文件。 方法一&#xff1a;“单元格右键…...

深入理解Tomcat:Java Web服务器的安装与配置

大家好&#xff01;今天我们来聊聊Java Web开发中最重要的工具之一——Apache Tomcat。Tomcat是一个开源的Java Servlet容器和Web服务器&#xff0c;它是运行Java Web应用程序的核心环境。无论是开发、测试还是部署Java Web应用&#xff0c;Tomcat都是不可或缺的工具。本文将详…...

【从零开始学习计算机科学】编译原理(五)语法制导翻译

【从零开始学习计算机科学】编译原理(五)语法制导翻译 语法制导翻译语法制导定义SDDSDD的求值顺序两类重要的SDD语法制导的翻译方案SDTSDT的实现L属性定义的SDT左递归翻译方案语法制导翻译 语法表述的是语言的形式,或者说是语言的样子和结构。而程序设计语言中另一方面,是…...

Git使用(二)--如何配置 GitHub 远程仓库及本地 Git 环境

在日常的开发过程中&#xff0c;使用版本控制工具 Git 是一个非常重要的技能&#xff0c;特别是对于管理和协作开发。通过 GitHub&#xff0c;我们可以轻松地进行代码版本管理和共享。这篇博客将带您一步步学习如何配置 Git 环境并将本地仓库与 GitHub 远程仓库连接起来。 一、…...

【MySQL是怎么运行的】0、名词解释

聚簇索引&#xff1a;聚簇索引和数据在一起&#xff0c;又名主键索引&#xff0c;是主键id构建的一颗B树&#xff0c;非叶节点是主键id&#xff0c;叶子节点是真实数据。其他索引统称二级索引&#xff0c;也称为非聚簇索引。覆盖索引&#xff1a;查找的数据就在索引树上&#x…...