多线程进阶——线程池的实现
什么是池化技术
池化技术是一种资源管理策略,它通过重复利用已存在的资源来减少资源的消耗,从而提高系统的性能和效率。在计算机编程中,池化技术通常用于管理线程、连接、数据库连接等资源。
我们会将可能使用的资源预先创建好,并且将它们创建在一个池中,当需要使用这些资源时,直接从池中获取,使用完毕后再将它们归还到池中,而不是每次都创建和销毁资源。
池化技术的引用场景十分广泛,例如线程池、数据库连接池、对象池等,今天我们主要要探讨的就是线程池
什么是线程池
线程池是一种典型的池化技术的应用,在我们日常使用多线程来处理任务时,如果每次都创建和销毁线程,频繁的创建与销毁线程会出现大量不必要的资源消耗,降低系统的性能。而在线程池中我们可以预先创建一定数量的线程,当需要执行任务时,直接从线程池中获取线程来执行任务,任务执行完毕后,线程并不会被销毁,而是继续保留在线程池中,等待下一次任务的执行,通过这种线程复用的方式,可以大大减少线程的创建和销毁,从而提高系统的性能和效率。
线程池的优点
-
避免频繁创建与销毁线程:线程池预先创建并维护一定数量的工作线程,避免了频繁创建和销毁线程带来的系统开销,特别是在处理大量短生命周期任务时,效果尤为显著。
-
负载均衡与缓存局部性:线程池可以根据任务负载动态调整线程工作状态,避免过度竞争和闲置。同时,线程在执行任务过程中可以充分利用CPU缓存,提高执行效率。
-
控制并发级别:通过限制线程池大小和任务队列容量,可以有效控制系统的并发级别,防止因过度并发导致的资源争抢和性能下降。
-
简化编程模型
-
线程池提供了一种简化的编程模型,开发者无需关心线程的创建、管理和销毁,只需将任务提交给线程池即可。这大大简化了多线程编程的复杂性,提高了开发效率。
-
线程池还提供了一些高级功能,如任务优先级、任务超时、任务取消等,这些功能可以帮助开发者更好地管理任务执行过程,提高系统的可靠性和稳定性。
-
线程池的组成部分
该线程池类的主要有以下组成部分
-
线程池类
线程池类主要负责管理线程池的状态并根据线程池的状态淘汰的管理工作线程的状态并且实现任务的异步提交- 工作线程类
- 有关线程池状态的枚举类
- 任务队列
task_queue
- 用来存放工作线程的列表
worker_list
- 控制线程池状态/工作线程列表/以及任务队列的互斥锁
- 用于唤醒/阻塞线程的条件变量
- 基于线程异步实现的任务提交函数
相关定义如下:
class ThreadPool{private:class worker_thread; // 工作线程类enum class status_t : std::int8_t{TERMINATED = -1,TERMINATING = 0,RUNNING = 1,PAUSED = 2,SHUTDOWN = 3}; // 线程池的状态: 已终止:-1,正在终止:0,正在运行:1,已暂停:2,等待线程池中任务完成,但是不接收新任务:3std::atomic<status_t> status; // 线程池的状态std::atomic<std::size_t> max_task_count; // 线程池中任务的最大数量std::shared_mutex status_mutex; // 线程池状态互斥锁std::shared_mutex task_queue_mutex; // 任务队列的互斥锁std::shared_mutex worker_lists_mutex; // 工作线程列表的互斥锁std::condition_variable_any task_queue_cv; // 任务队列的条件变量std::condition_variable_any task_queue_cv_full; // 任务队列满的条件变量std::condition_variable_any task_queue_cv_empty; // 任务队列空的条件变量std::queue<std::function<void()>> task_queue; // 任务队列,其中存储待执行的任务std::list<worker_thread> worker_lists; // 工作线程列表// 考虑到为了确保线程池的唯一性和安全性,禁止使用拷贝赋值与移动赋值ThreadPool(ThreadPool &) = delete;ThreadPool &operator=(ThreadPool &) = delete;ThreadPool(ThreadPool &&) = delete;ThreadPool &operator=(ThreadPool &&) = delete;// 在取得对状态变量的访问权后,调用下列函数来改变线程池的状态void pause_with_status_lock(); // 暂停线程池void resume_with_status_lock(); // 恢复线程池void shutdown_with_status_lock(); // 立刻关闭线程池void shutdown_with_wait_status_lock(bool wait_for_tasks); // 等待任务执行完毕关闭线程池void terminate_with_status_lock(); // 终止线程池void wait_with_status_lock(); // 等待所有任务执行完毕public:ThreadPool(std::size_t max_task_count,std::size_t inital_thread_count=0); // 构造函数~ThreadPool(); // 析构函数template <typename Func, typename... Args>auto submit(Func &&f, Args &&...args) -> std::future<decltype(f(args...))>; // 提交任务,实现对线程任务的异步提交void pause(); // 暂停线程池void resume(); // 恢复线程池void shutdown(); // 立刻关闭线程池void shutdown_wait(); // 等待任务执行完毕关闭线程池void terminate(); // 终止线程池void wait(); // 等待所有任务执行完毕void add_thread(std::size_t count); // 增加线程void remove_thread(std::size_t count); // 删除线程void set_max_task_count(std::size_t count);std::size_t get_task_count(); // 获取任务数量std::size_t get_thread_count(); // 获取线程数量};template <typename Func, typename... Args>auto ThreadPool::submit(Func &&f, Args &&...args) -> std::future<decltype(f(args...))>{std::shared_lock<std::shared_mutex> status_lock(status_mutex);switch (status.load()){case status_t::TERMINATED:throw std::runtime_error("ThreadPool is terminated");case status_t::TERMINATING:throw std::runtime_error("ThreadPool is terminating");case status_t::PAUSED:throw std::runtime_error("ThreadPool is paused");case status_t::SHUTDOWN:throw std::runtime_error("ThreadPool is shutdown");case status_t::RUNNING:break;}if(max_task_count > 0&&max_task_count.load()==get_task_count())throw std::runtime_error("ThreadPool is full");using return_type=decltype(f(args...));auto task=std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<Func>(f), std::forward<Args>(args)...));std::unique_lock<std::shared_mutex> lock2(task_queue_mutex);task_queue.emplace([task](){ (*task)(); }); lock2.unlock();status_lock.unlock();task_queue_cv.notify_one(); //唤醒一个线程来执行当前任务std::future<return_type> res=task->get_future();return res;}
-
工作线程类
工作线程类主要负责从任务队列中取出任务并执行,同时处理根据当前自身线程的状态变化动态调整线程池的状态。- 有关工作线程状态的枚举类
- 线程状态的原子变量
- 控制线程阻塞/唤醒的condition_variable
相关定义如下:
class ThreadPool::worker_thread{private:enum class status_t:int8_t{TERMINATED=-1,TERMINATING=0,PAUSE=1,RUNNING=2,BLOCKED=3}; // 1- 线程已终止 0- 线程正在终止 1- 线程已暂停 2- 线程正在运行 3- 线程已阻塞,等待任务中ThreadPool *pool; //指向线程池std::atomic<status_t> status; //线程状态std::shared_mutex status_mutex; //线程状态互斥锁std::binary_semaphore sem; //信号量,要来控制线程的阻塞和唤醒std::thread thread; //工作线程//禁用拷贝构造与移动构造以及相关复赋值worker_thread(const worker_thread &) = delete;worker_thread(worker_thread &&) = delete;worker_thread &operator=(const worker_thread &) = delete;worker_thread &operator=(worker_thread &&) = delete;void resume_with_status_lock();status_t terminate_with_status_lock();void pause_with_status_lock();public:worker_thread(ThreadPool *pool);~worker_thread();void pause();void resume();status_t terminate();};
线程池的工作机理剖析
对于该项目的线程池而言,它主要存在下面三个机制:
- 基于状态机的状态转换
- 任务的异步执行机制
- 工作线程的任务执行
- 基于状态机的状态转换
在线程池中主要有两种状态转换,首先是线程池的状态转换,这里我们通过枚举定义了线程池的不同状态,让线程池在这些状态中不断切换,枚举类的定义如下:
enum class status_t : std::int8_t{TERMINATED = -1,TERMINATING = 0,RUNNING = 1,PAUSED = 2,SHUTDOWN = 3};
// 线程池的状态: 已终止:-1,正在终止:0,正在运行:1,已暂停:2,等待线程池中任务完成,但是不接收新任务:3std::atomic<status_t> status;//线程池的状态,这里使用原子变量来保证操作的原子性,实现了线程安全
而线程池的状态机模型如下:
具体的状态转化代码可以参考:
void ThreadPool::pause_with_status_lock(){switch (status.load()){case status_t::TERMINATED: // 线程池已经终止case status_t::TERMINATING: // 线程池正在终止case status_t::PAUSED: // 线程池已经暂停case status_t::SHUTDOWN: // 线程池已经关闭return;case status_t::RUNNING:status.store(status_t::PAUSED);break;default:throw std::runtime_error("unknown status");}std::unique_lock<std::shared_mutex> lock(worker_lists_mutex);for (auto &worker : worker_lists){worker.pause();}}
工作线程的状态转换就简单了,只有四种状态:
enum class status_t:int8_t{TERMINATED=-1,TERMINATING=0,PAUSE=1,RUNNING=2,BLOCKED=3}; // 1- 线程已终止 0- 线程正在终止 1- 线程已暂停 2- 线程正在运行 3- 线程已阻塞,等待任务中
它的状态转换基本上和线程池息息相关,可以参考线程池的状态转换,这里不做过多说明。
- 任务的异步执行
在讲线程的异步执行之前,先来了解一下什么是线程的异步执行:
在计算机科学中,“异步”通常指的是一个操作在发起之后,不需要等待其完成就可以继续执行后续的操作。异步编程模型允许程序在等待某个耗时操作完成的同时继续执行其他任务,从而提高整体效率和响应速度。
在c++11中为了实现异步执行,引入了std::async,它可以在一个单独的线程中执行一个函数,并返回一个std::future对象,通过这个对象可以获取函数的返回值。这里不做赘述,后面我们单独出一篇文章来探讨这个问题,这里我们只需要知道它的作用就可以了。这里我们来看一下线程池的任务提交函数submit
:
template <typename Func, typename... Args>auto ThreadPool::submit(Func &&f, Args &&...args) -> std::future<decltype(f(args...))>{std::shared_lock<std::shared_mutex> status_lock(status_mutex);switch (status.load()){case status_t::TERMINATED:throw std::runtime_error("ThreadPool is terminated");case status_t::TERMINATING:throw std::runtime_error("ThreadPool is terminating");case status_t::PAUSED:throw std::runtime_error("ThreadPool is paused");case status_t::SHUTDOWN:throw std::runtime_error("ThreadPool is shutdown");case status_t::RUNNING:break;}if(max_task_count > 0&&max_task_count.load()==get_task_count())throw std::runtime_error("ThreadPool is full");using return_type=decltype(f(args...));auto task=std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<Func>(f), std::forward<Args>(args)...));std::unique_lock<std::shared_mutex> lock2(task_queue_mutex);task_queue.emplace([task](){ (*task)(); }); lock2.unlock();status_lock.unlock();task_queue_cv.notify_one(); //唤醒一个线程来执行当前任务std::future<return_type> res=task->get_future();return res;}
它的实现思路如下:
- 首先查看当前线程池的状态,如果不是RUNNING状态,抛出异常
- 查看当前的任务队列是否已满,如果已满,则抛出异常
- 将任务转换为std::packaged_task对象,并将其包装为std::function对象,以便在线程池中执行,这里使用了std::forward将参数传递给任务函数实现完美转发,通过std::function对象将任务函数包装std::function<void()>类型,以便在线程池中通过在工作线程中可以用统一的格式(直接用 () 进行调用)对任何形式的任务进行调用执行。
- 将std::packaged_task对象添加到任务队列中,并返回一个std::future对象,该对象可以用于获取任务函数的返回值
- 工作线程的任务执行
在讲解工作线程的任务执行之前,我们先来看一下它的具体实现:
ThreadPool::worker_thread::worker_thread(ThreadPool *pool):pool(pool),status(status_t::RUNNING),sem(0),thread([this](){while (true){// 实现线程状态的判断,决定是否由该线程执行任务std::unique_lock<std::shared_mutex> unique_lock_status(this->status_mutex);while (true){if (!unique_lock_status.owns_lock()) // 当锁被释放时,重新获取锁{unique_lock_status.lock();}bool break_flag = false; // 当线程为运行态的时候,跳出循环switch (this->status.load()){case status_t::TERMINATING:this->status.store(status_t::TERMINATED);case status_t::TERMINATED:return;case status_t::RUNNING:break_flag = true;break;case status_t::PAUSE: // PAUSE状态下需要其他的线程来唤醒该线程需要解锁避免出现死锁unique_lock_status.unlock();this->sem.acquire(); // 阻塞当前线程break;case status_t::BLOCKED: // 不支持Blockeddefault:unique_lock_status.unlock();throw std::runtime_error("invalid status");}if (break_flag){unique_lock_status.unlock();break;}}// 判断队列是否为空,如果为空则阻塞当前线程std::unique_lock<std::shared_mutex> unique_lock_task(this->pool->task_queue_mutex);while (this->pool->task_queue.empty()){while (true){if (!unique_lock_status.owns_lock()){unique_lock_status.lock();}bool break_flag = false;switch (this->status.load()){case status_t::TERMINATING:status.store(status_t::TERMINATED);case status_t::TERMINATED:return;case status_t::PAUSE:unique_lock_task.unlock();unique_lock_status.unlock();this->sem.acquire(); // 阻塞线程break;case status_t::RUNNING:this->status.store(status_t::BLOCKED);case status_t::BLOCKED:break_flag = true;break;default:unique_lock_status.unlock();unique_lock_task.unlock();throw std::runtime_error("invalid status");}if (break_flag) // 若为阻塞状态等待唤醒{unique_lock_status.unlock();break;}}this->pool->task_queue_cv.wait(unique_lock_task);while (true){if (!unique_lock_status.owns_lock()){unique_lock_status.lock();}bool break_flag = false;switch (this->status.load()){case status_t::TERMINATING:status.store(status_t::TERMINATED);case status_t::TERMINATED:return;case status_t::PAUSE:unique_lock_task.unlock();unique_lock_status.unlock();this->sem.acquire(); // 阻塞线程break;case status_t::BLOCKED:this->status.store(status_t::RUNNING);case status_t::RUNNING:break_flag = true;break;default:unique_lock_status.unlock();throw std::runtime_error("invalid status");}if (break_flag) // 若为阻塞状态等待唤醒{unique_lock_status.unlock();break;}}}// 尝试取出任务并执行try{auto task = this->pool->task_queue.front();this->pool->task_queue.pop();if (this->pool->task_queue.empty()){this->pool->task_queue_cv_empty.notify_all();}unique_lock_task.unlock();task();}catch (const std::exception &e){std::cerr<<e.what()<<std::endl;}}}){}
工作线程工作的进行是定义在线程构造函数中,即线程开始工作后,会一直执行这个函数,直到线程被销毁,它的实现逻辑是:
- 确定工作线程状态,决定是否由该线程执行任务
- 查看工作队列是否为空,不为空则取出一个任务
- 执行任务
- 根据线程池状态变更,如接收到暂停、恢复、终止等指令,工作线程调整自身状态并执行相应操作
拓展:ini文件的读写
考虑到线程池后续想要对其进行拓展,例如加入epoll进行网络通信,所以我实现了一个简单的ini解析器,他主要有两部分:
- Value类:实现了
int,double,long,string
等基础类型与Value对象之间的互相转换,代码如下:
class Value // 封装一个Value对象,支持string、int、double、bool类型,可以实现多个类型转换为Value对象,并且可以转换为多个类型{private:std::string m_value; public://支持各个类型的构造函数Value();Value(const std::string& value);Value(const int value);Value(const double value);Value(const bool value);Value(const char* value);~Value();//赋值操作,支持任意类型Value& operator=(const std::string& value);Value& operator=(const int value);Value& operator=(const double value);Value& operator=(const bool value);Value& operator=(const char* value);//对于值的判断bool operator==(const Value& other);bool operator!=(const Value& other);//这里实现可以将Value对象转换为任意类型operator int();operator double();operator bool();operator std::string();};
- IniFile:基于嵌套map来存储ini文件的各个分区与分区的键值对,实现如下:
typedef std::map<std::string, Value> Section;class IniFile{private:std::string m_filename;std::map<std::string, Section> m_sections;std::string trim(std::string s); //去除字符串两边的空格 public:IniFile();IniFile(const std::string& filename);~IniFile();bool load(const std::string& filename); //加载ini文件bool save(const std::string& filename); //保存ini文件void show(); //显示ini文件内容void clear(); //清空ini文件内容Value& get(const std::string& section,const std::string& key); //获取指定分区指定键的值void set(const std::string& section,const std::string& key,const Value& value); //设置指定分区指定键的值bool remove(const std::string& section,const std::string& key); //删除指定分区指定键的值bool has(const std::string& section,const std::string& key); //判断键值对是否存在bool has(const std::string& section);// 判断分区是否存在Section& operator[](const std::string& section); // 重载[]操作符,用来访问分区中的键值std::string str(); //将字符串转换为ini文件格式};
篇幅有限,就不粘详细代码了,大家也可以尝试自己实现以下,毕竟cpp就是一个不断遭轮子的过程。
结语
线程池的实现是一个复杂的过程,需要考虑多线程并发、任务调度、线程管理等多个方面。本文通过一个简单的线程池实现,介绍了线程池的基本原理和实现方法。希望本文对您有所帮助。
- 博客地址
- 教程地址
- 项目地址
相关文章:

多线程进阶——线程池的实现
什么是池化技术 池化技术是一种资源管理策略,它通过重复利用已存在的资源来减少资源的消耗,从而提高系统的性能和效率。在计算机编程中,池化技术通常用于管理线程、连接、数据库连接等资源。 我们会将可能使用的资源预先创建好,…...

C++网络编程之C/S模型
C网络编程之C/S模型 引言 在网络编程中,C/S(Client/Server,客户端/服务器)模型是一种最基本且广泛应用的架构模式。这种模型将应用程序分为两个部分:服务器(Server)和客户端(Clien…...

目标检测:YOLOv11(Ultralytics)环境配置,适合0基础纯小白,超详细
目录 1.前言 2. 查看电脑状况 3. 安装所需软件 3.1 Anaconda3安装 3.2 Pycharm安装 4. 安装环境 4.1 安装cuda及cudnn 4.1.1 下载及安装cuda 4.1.2 cudnn安装 4.2 创建虚拟环境 4.3 安装GPU版本 4.3.1 安装pytorch(GPU版) 4.3.2 安装ultral…...

面试域——岗位职责以及工作流程
摘要 介绍互联网岗位的职责以及开发流程。在岗位职责方面,详细阐述了产品经理、前端开发工程师、后端开发工程师、测试工程师、运维工程师等的具体工作内容。产品经理负责需求收集、产品规划等;前端专注界面开发与交互;后端涉及系统架构与业…...

C#文件内容检索的功能
为了构建一个高效的文件内容检索系统,我们需要考虑更多的细节和实现策略。以下是对之前技术方案的扩展,以及一个更详细的C# demo示例,其中包含索引构建、多线程处理和文件监控的简化实现思路。 扩展后的技术方案 索引构建: 使用L…...

Redis-05 Redis发布订阅
Redis 的发布订阅(Pub/Sub)是一种消息通信模式,允许客户端订阅消息频道,以便在发布者向频道发送消息时接收消息。这种模式非常适合实现消息队列、聊天应用、实时通知等功能。 #了解即可,用的很少...

【读书笔记·VLSI电路设计方法解密】问题27:什么是可制造性设计
尽管业界尚未达成共识,但“可制造性设计”这一术语大致描述了旨在提高产品良率的特定分析、预防、纠正和验证工作。这不同于后GDSII阶段的分辨率增强技术,如光学邻近效应校正(OPC)和相位移掩膜(PSM)。“可制造性设计”中的关键词是“设计”,意指在设计阶段(而非设计完成…...

数据结构:堆的应用
堆排序 假定有一组数据极多的数,让我们进行排序,那我们很容易想到一种经典的排序方法,冒泡排序,我们对冒泡排序的时间复杂度进行分析: 显然,冒泡排序的时间复杂度是O(n^2),当数据量…...

Spring Boot 实现文件分片上传和下载
文章目录 一、原理分析1.1 文件分片1.2 断点续传和断点下载1.2 文件分片下载的 HTTP 参数 二、文件上传功能实现2.1 客户端(前端)2.2 服务端 三、文件下载功能实现3.1 客户端(前端)3.2 服务端 四、功能测试4.1 文件上传功能测试4.2 文件下载功能实现 参考资料 完整案例代码&…...

夹逼准则求数列极限(复习总结)
记住这两个准则,然后我们就开始看题目 因为是证明题,所以要放缩到什么值已经是确定的了。也就是放缩到0,然后很明显地可以看出前面已经有一个可以使得极限是0了,并且后面的值明显小于1,就是逐渐缩小的趋势,…...

【python】OpenCV—WaterShed Algorithm(1)
文章目录 1、功能描述2、代码实现3、完整代码4、效果展示5、涉及到的库函数5.1、cv2.pyrMeanShiftFiltering5.2、cv2.morphologyEx5.3、cv2.distanceTransform5.4、cv2.normalize5.5、cv2.watershed 6、参考 1、功能描述 基于分水岭算法对图片进行分割 分水岭分割算法&#x…...

查找与排序-插入排序
思考:在把待排序的元素插入已经有序的子序列中时,是不是一定要逐一比较?有没有改进方法? 在查找插入位置的时候可以采用折半(二分)搜索的办法。 一、折半插入排序 1.折半插入排序算法的基本思想 假设待…...

JAVA基础:多线程 (学习笔记)
多线程 一,什么是线程? 程序:为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码进程:程序的一次执行过程。 正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的…...

盲盒小程序/APP系统,市场发展下的新机遇
当下,年轻人热衷于各种潮玩商品,尤其是一盲盒为主的潮流玩具风靡市场,吸引了众多入局者。随着互联网信息技术的快速发展,各类线上盲盒小程序又进一步推动了盲盒市场的发展,成为年轻人拆盲盒的主要阵地。在盲盒经济中&a…...

Unity3D LayoutGroup组件详解
Unity3D中的LayoutGroup组件是一种强大的工具,用于动态调整UI元素的布局。它主要包括三种类型:Horizontal Layout Group(水平布局组)、Vertical Layout Group(垂直布局组)和Grid Layout Group(网…...

[NeetCode 150] Foreign Dictionary
Foreign Dictionary There is a foreign language which uses the latin alphabet, but the order among letters is not “a”, “b”, “c” … “z” as in English. You receive a list of non-empty strings words from the dictionary, where the words are sorted lex…...

小新学习K8s第一天之K8s基础概念
目录 一、Kubernetes(K8s)概述 1.1、什么是K8s 1.2、K8s的作用 1.3、K8s的功能 二、K8s的特性 2.1、弹性伸缩 2.2、自我修复 2.3、服务发现和负载均衡 2.4、自动发布(默认滚动发布模式)和回滚 2.5、集中化配置管理和密钥…...

如何用终端批量修改一个文件夹里面所有图片的后缀名?
步骤: winr ,然后输入cmd,打开终端 使用cd命令导航到要修改图片后缀名的文件夹。eg.我的该文件夹(C:\dog)下,保存的图片。(cd和文件目录之间要有空格)批量改变后缀名,假如让后缀名全部要从 ".webp&q…...

关于AI网络架构的文章
思科OCP anounce了800G 51.2T G200-based minipack3 switch。对比之前Tesla anounce的TTPoE。真的很好奇,谁是AI-networking的未来,以及思科是否走在正确的路上,以及S1背后的技术。 大致浏览了相关的文章,先mark住,回…...

【ChatGPT】在多轮对话中引导 ChatGPT 保持一致性
在多轮对话中引导 ChatGPT 保持一致性 多轮对话是与 ChatGPT 等对话模型互动时的一大特点,特别是在复杂任务和长时间对话中,保持对话的一致性显得尤为重要。用户往往希望 ChatGPT 能够在上下文中理解先前的对话内容,避免反复重申问题或者给出…...

【Chapter 7】因果推断中的机器学习:从T-学习器到双重稳健估计
随着机器学习技术的发展,数据科学家们开始探索如何将这些先进的方法应用于因果推断问题,尤其是处理异质性效应(Effect Heterogeneity)时。本章将介绍几种基于机器学习的因果推断方法,包括T-学习器、X-学习器和双重稳健…...

vim的使用方法
常见的命令可参考: Linux vi/vim | 菜鸟教程www.runoob.com/linux/linux-vim.html编辑https://link.zhihu.com/?targethttps%3A//www.runoob.com/linux/linux-vim.html 1. vim的工作模式 vi/vim 共分为三种模式,命令模式、编辑输入模式和末行&am…...

OPPO携手比亚迪共同探索手机与汽车互融新时代
10月23日,OPPO与比亚迪宣布签订战略合作协议,双方将共同推进手机与汽车的互融合作,这一合作也标志着两大行业巨头在技术创新和产业融合上迈出了重要一步,为手机与汽车的深度融合探索新的可能。 OPPO创始人兼首席执行官陈明永、OP…...

Apache Linkis:重新定义计算中间件
在大数据技术蓬勃发展的今天,我们见证了从单一计算引擎到多元化计算范式的演进。然而,随着企业数据应用场景的日益丰富,一个严峻的挑战逐渐显现:如何有效管理和协调各类计算引擎,使其能够高效协同工作?Apac…...

go gorm简单使用方法
GORM 是 Go 语言中一个非常流行的 ORM(对象关系映射)库,它允许开发者通过结构体来定义数据库表结构,并提供了丰富的 API 来操作数据库。 安装 go get -u gorm.io/gorm go get -u gorm.io/driver/sqlite表结构 在 gorm 中定义表结…...

【c++高级篇】--多任务编程/多线程(Thread)
目录 1.进程和线程的概念: 1.1 进程(Process): 1.2线程(Thread): 1.3 对比总结: 2.多线程编程: 2.1 基于线程的多任务处理(Thread)…...

【力扣专题栏】两数相加,如何实现存储在链表中的整数相加?
题解目录 1、题目描述解释2、算法原理解析3、代码编写(原始版本)4、代码编写(优化版本) 1、题目描述解释 2、算法原理解析 3、代码编写(原始版本) /*** Definition for singly-linked list.* struct ListN…...

SOLID - 接口隔离原则(Interface Segregation Principle)
SOLID - 接口隔离原则(Interface Segregation Principle) 定义 接口隔离原则(Interface Segregation Principle,ISP)是面向对象设计中的五个基本原则之一,通常缩写为SOLID中的I。这一原则由Robert C. Martin提出&…...

arrylist怎么让他变得不可修改
在Java中,要将一个 ArrayList变得不可修改,你可以使用以下几种方法: ###1. 使用 Collections.unmodifiableList Java 提供了 Collections.unmodifiableList 方法,可以生成一个不可修改的视图。这种方式返回的列表将不允许添加、…...

SpringMVC实战(3):拓展
四、RESTFul风格设计和实战 4.1 RESTFul风格概述 4.1.1 RESTFul风格简介 RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量级的通信协议&…...