【C++11】多线程
- 多线程
- 创建线程
- thread提供的成员函数
- 获取线程id的方式
- 线程函数参数的问题
- 线程join场景和detach
- 互斥量库(mutex)
- mutex
- recursive_mutex
- lock_guard 和 unique_lock
- 原子性操作库(atomic)
- 条件变量库(condition_varuable)
- 综合案例(实现两个线程交替打印1-100)
多线程
在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念
创建线程

- 调用无参的构造函数创建
thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何的线程函数对象,也没有启动任何线程。
thread t1;
#include <iostream>
#include <thread>
using namespace std;void func(int n)
{cout << n << endl;
}int main()
{thread t1;t1 = thread(func, 10);t1.join();return 0;
}
我们的thread是提供了移动赋值函数的,所以,当后序需要让该线程关联线程函数的时候,我们可以定义一个匿名的线程,然后调用移动赋值传给他

thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。
- 调用带参的构造函数
thread的带参构造函数的定义如下:
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
参数说明:
- fn:可调用对象,比如:仿函数,指针,lambda表达式,被包装器包装后的可调用对象等。
- args:进行对fn进行传值。
调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。比如:
void Func(int n, int num)
{for (int i = 0; i < n; i++){cout <<num<<":" << i << endl;}cout << endl;
}int main()
{thread t2(Func, 10,666);t2.join();return 0;
}

结合之前的lambda函数,这么我们可以很明显的看到lambda的作用
int main()
{//thread t2(Func, 10,666);thread t2([](int n = 10, int num = 666){for (int i = 0; i < n; i++){cout << num << ":" << i << endl;}cout << endl;});t2.join();return 0;
}
其输出结果和上图是一样的,注意,这么线程的形参只有一个仿函数lambda
使用一个容器来保存线程
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{size_t m;//线程个数cin >> m;vector<thread> vthread(m);//直接初始化长度为10for (int i = 0; i < m; ++i){vthread[i] = thread([i](){cout << "我是第" << i << "个线程" << endl;});}for (auto& e : vthread){e.join();}return 0;
}

thread提供的成员函数
在我们多线程中,常用的成员函数如下:
join:对该线程进行等待,在等待的线程返回之前,调用join将会将线程进行阻塞,我们主要阻塞的是主线程。joinable:判断该线程是否已经执行完毕,如果是则返回true,否则返回false。
detach:将该线程进行分离主线程,被分离后,不在需要创建线程的主线程调用join进行对其等待get_id:获取该线程的id
此外,joinable函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:
- 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
- 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
- 线程已经调用join或detach结束。(线程已经结束)
获取线程id的方式
其实调用线程id的方法有两种,实际情况我们看下边的代码:
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{size_t m;//线程个数cin >> m;vector<thread> vthread(m);//直接初始化长度为10for (int i = 0; i < m; ++i){vthread[i] = thread([i](){printf("我是第%d个线程\n", i);cout << this_thread::get_id() << endl;});}for (auto& e : vthread){cout << e.get_id() << endl;}for (auto& e : vthread){e.join();}return 0;
}

- 两者调用ID的环境是不同的
- 从代码中我们可以看到
get_id是需要线程对象来调用的 - 但是
this_thread::get_id我们通过多线程提供的特殊窗口可以不通过线程对象就可以直接调用
this_thread命名空间中还提供了以下三个函数:
- yield:当前线程“放弃”执行,让操作系统调度另一线程继续执行
- sleep_until :让当前线程休眠到一个具体时间点
- sleep_for:让当前线程休眠一个时间段
线程函数参数的问题
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:
void add(int& num)
{num++;
}
int main()
{int num = 0;thread t(add, num);t.join();cout << num << endl; //0return 0;
}
解决其办法有三种:
-
方式一:借助std::ref函数
-

-
方式二:地址的拷贝
-

-
方式三:借助lambda表达式
-

线程join场景和detach
当启动线程后,如果不使用join进行阻塞等待的话程序就会直接报错,因为此时存在内存泄漏问题。
thread库给我们提供了一共两种回收线程的方法
join方式
使用join进行线程回收时,一个线程只能回收一个,如果进行多次回收就会直接报错
但是如果你对一个线程回收后,又对此线程再一次的进行了移动赋值,那么此时还可以再一次的对线程进行二次join,如下代码案例:
void func(int n = 20)
{cout << n << endl;
}int main()
{thread t(func, 20);t.join();t = thread([](int num = 10) {cout << num << endl; });t.join();
}

需要注意的是,如果线程运行起来后,线程的内部发生了异常,那么我们锁设置的阻塞就起不到任何作用了,
detach方式
主线程创建新线程后,也可以调用detach进行将线程和主线程分离,分离后,新线程会到后台运行,其所有权和控制权将会交给C++运行库,此时,c++运行库会保证当线程退出时,其相关资源能够被正确回收
#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{mtx.lock();for (int i = 0; i < 100000; i++){x++;}std::cout << "Hello from detached thread:" << n << std::endl;mtx.unlock();
}int main()
{std::thread t1(threadFunc,11111);std::thread t2(threadFunc,22222);t1.detach(); // 将线程设置为可分离的t2.detach(); // 将线程设置为可分离的// 主线程继续执行其他任务std::cout << "Main thread continues..." << std::endl;// 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完std::this_thread::sleep_for(std::chrono::seconds(1));cout << x << endl;return 0;
}

互斥量库(mutex)
在我们的c++11中,我们一共提供了四种互斥锁形式
mutex
1. std::mutex
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不可以进行拷贝,也不能进行移动。
mutex中常用的成员函数如下:
- lock:对互斥量进行加锁
- unlock:对互斥量进行解锁,释放互斥量所有权
- try_lock:尝试对互斥量进行加锁。
线程函数调用lock时,可能会发生三种情况:
- 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用
unlock后,才对其进行解锁。 - 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会进入阻塞状态。
- 如果该互斥量被当前调用线程锁住,则会产生死锁
线程函数调用try_lock时,可能会发生三种情况:
- 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用
unlock后,才对其进行解锁。 - 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会返回false,并不会对其进行阻塞。
- 如果该互斥量被当前调用线程锁住,则会产生死锁
在没有对临界资源加锁的时候,由于是多个进程同时进行,这时,不能同步的,正确的完成我们的任务,此时我们就需要给临界资源进行加锁

正确做法
#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{mtx.lock();for (int i = 0; i < 100000; i++){x++;}std::cout << "Hello from detached thread:" << n << std::endl;mtx.unlock();
}int main()
{std::thread t1(threadFunc,11111);std::thread t2(threadFunc,22222);t1.detach(); // 将线程设置为可分离的t2.detach(); // 将线程设置为可分离的// 主线程继续执行其他任务std::cout << "Main thread continues..." << std::endl;// 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完std::this_thread::sleep_for(std::chrono::seconds(1));cout << x << endl;return 0;
}

recursive_mutex
2. std::recursive_mutex
recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。
- 有一些线程避免不了它是递归函数,但是如果对递归里的临界资源进行加锁时,可能会持续申请自己还还未释放的锁,进而导致死锁问题。
- 而recursive_mutex允许同一个线程对互斥量进行多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock。
int x = 0;
recursive_mutex mtx;void Func(int n)
{if (n == 0)return;//递归锁的原理就是当调用自己本身时,发现给自己上锁的还是自己,这时会自动解锁,通过此种方法来进行重复加锁解锁mtx.lock();++x;Func(n - 1);mtx.unlock();
}int main()
{thread t1(Func, 1000);thread t2(Func, 2000);t1.join();t2.join();cout << x << endl;return 0;
}

后两种可以查看收藏
lock_guard 和 unique_lock
为什么要使用lock_guard 和 unique_lock呢?
在我们平时使用锁中,如果锁的范围比较大,那么极度有可能在中途时忘记解锁,此后申请这个锁的线程就会被阻塞住,也就造成了死锁的问题,例如:
mutex mtx;
void func()
{mtx.lock();//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //中途返回(未解锁)}//...mtx.unlock();
}
int main()
{func();return 0;
}
因此使用互斥锁时如果控制不好就会造成死锁,最常见的就是此处在锁中间代码返回,此外还有一个比较常见的情况就是在锁的范围内抛异常,也很容易导致死锁问题。
因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。
为此c++11推出了解决方案,采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。
lock_guard
lock_guard是C++11中的一个模板类,其定义如下:
template <class Mutex>
class lock_guard;
通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。比如:
mutex mtx;
void func()
{lock_guard<mutex> lg(mtx); //调用构造函数加锁//...FILE* fout = fopen("data.txt", "r");if (fout == nullptr){//...return; //调用析构函数解锁}//...
} //调用析构函数解锁
int main()
{func();return 0;
}
模拟实现lock_guard
模拟实现lock_guard类的步骤如下:
- lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
- 调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的lock函数进行加锁。
- lock_guard的析构函数中调用互斥锁的unlock进行解锁。
- 需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的
class Lock_guard
{
public:Lock_guard(mutex& mtx):_mtx(mtx){_mtx.lock(); //加锁}~Lock_guard(){_mtx.unlock();//解锁}Lock_guard(const Lock_guard&) = delete;Lock_guard& operator=(const Lock_guard&) = delete;
private:mutex& _mtx;
};mutex mtx;
int x;
int main()
{thread t1([](){Lock_guard lg(mtx);x = 1;});t1.join();cout << x << endl;
}

unique_lock
比起lock_guard来说,unique_lock的逻辑和lock_guard的逻辑是一样的,都是调用类时加锁,出作用域时解锁,但是unique_lock比起lock_guard多加了几个条件,分别是:
- 加锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
- 修改操作:移动赋值、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
- 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。
具体实现逻辑,我们看下边的交替打印100的案例。
原子性操作库(atomic)
在上面的加锁案例中,多个线程同时对全局变量x进行++操作,并且,我们还对此操作进行了加锁,主要原因时++操作不是原子的,原子究竟是个什么呢?
原子大概的来说只有两个状态,一种是在运行,一种是不在运行,当处于在运行状态时,当其他进程进来时,发现为在运行状态就会进行等待, 知道状态模式换了才会进入下一个线程。

int main()
{int n = 100000;atomic<int> x = 0;//atomic<int> x = {0};//atomic<int> x{0};//int x = 0;mutex mtx;size_t begin = clock();thread t1([&, n](){for (int i = 0; i < n; i++){++x;}});thread t2([&, n]() {for (int i = 0; i < n; i++){++x;}});t1.join();t2.join();cout << x << endl;return 0;
}

条件变量库(condition_varuable)
condition_variable中提供的成员函数,可分为wait系列和notify系列两类。
wait系列成员函数(wait自带解锁功能和阻塞功能)
wait系列成员函数的作用就是让调用线程进行排队阻塞等待,包括wait、wait_for和wait_until。
下面先以wait为例子,wait函数提供了两个不同版本的接口:
//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);
函数说明:
- 调用第一个版本的时候,需要传入的参数只有一个互斥锁,线程调用wait后,就会进入阻塞状态,直到被唤醒
- 调用第二个版本的时候,不仅要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。
实际wait具有两个功能
- 一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。
- 当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。
notify系列成员函数
notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。
- notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
- notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。
综合案例(实现两个线程交替打印1-100)
尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。
该题目主要考察的就是线程的同步和互斥。
#include<mutex>
#include<condition_variable>int main()
{mutex mtx;condition_variable cv;int n = 100;int x = 1;// 问题1:如何保证t1先运行,t2阻塞?// 问题2:如何防止一个线程不断运行?thread t1([&, n]() {while (1){unique_lock<mutex> lock(mtx);if (x >= 100)break;//if (x % 2 == 0) // 偶数就阻塞//{// cv.wait(lock);//}cv.wait(lock, [&x]() {return x % 2 != 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}});thread t2([&, n]() {while (1){unique_lock<mutex> lock(mtx);if (x > 100)break;//if (x % 2 != 0) // 奇数就阻塞//{// cv.wait(lock);//}cv.wait(lock, [&x](){return x % 2 == 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();}});t1.join();t2.join();return 0;
}

相关文章:
【C++11】多线程
多线程创建线程thread提供的成员函数获取线程id的方式线程函数参数的问题线程join场景和detach 互斥量库(mutex)mutexrecursive_mutexlock_guard 和 unique_lock 原子性操作库(atomic)条件变量库(condition_varuable&a…...
【vue3】shallowReactive与shallowRef;readonly与shallowReadonly;toRaw与markRaw
假期第六篇,对于基础的知识点,我感觉自己还是很薄弱的。 趁着假期,再去复习一遍 1、shallowReactive与shallowRef shallowReactive:只处理对象最外层属性的响应式(浅响应式) shallowRef:只处理…...
手机建模教程 | 如何从易模App中导出模型?有哪些格式?含贴图吗?
很多小伙伴使用易模App是为了能快速地将已有实物的物体“变成”三维模型后转到自己习惯的3D软件中去编辑,于是,大家都关心模型能否导出,以及导出格式有没有自己想要的? 博雅仔告诉大家,当然可以导出! 在导出…...
数据分析技能点-机器学习优化思想
优化思想,这个听起来极其专业和高端的词汇,其实它无处不在,悄无声息地影响着我们的生活和决策。从寻找最快的上班路线,到决定如何配置投资组合,优化思想都是一个不可或缺的元素。而在机器学习领域,优化思想更是扮演着至关重要的角色。 文章目录 优化的基础优化问题与实际…...
应用架构的演进:亚马逊的微服务实践
当你在亚马逊上购物时,或许不会想到,你看到的这个购物网站,其背后技术架构经历了什么样的变迁与升级。 还记得上世纪 90 年代,那个只卖书的网上书店吗?那时的亚马逊,不过是一个架构简单的网站,所有的功能都堆积在一个庞大的软件堡垒里。随着更多业务的增加、更新和迭代,这个软…...
leetCode 55.跳跃游戏 贪心算法
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。 示例 1: 输入…...
CF505B Mr. Kitayuta‘s Colorful Graph
Mr. Kitayuta’s Colorful Graph 题面翻译 给出一个 n n n 个点, m m m 条边的无向图,每条边上是有颜色的。有 q q q 组询问 对于第 i i i 组询问,给出点对 u i , v i u_i,v_i ui,vi。求有多少种颜色 c c c 满足:有至…...
c#设计模式-结构型模式 之 组合模式
🚀简介 组合模式又名部分整体模式,是一种 结构型设计模式 ,是用于把一组相似的对象当作一个 单一的对象 。组合模式 依据树形结构来组合对象 ,用来表示部分以及整体层,它可以让你将对象组合成树形结构,并且…...
【Rust日报】2023-09-30 使用Rust做web抓取
CockroachDB 用rust重新实现 嘿,伙计们,我在 Rust 中实现了一个分布式 SQL 数据库。它就像 CockroachDB 和谷歌Google Spanner。告诉我你的想法。 注意: 这不是生产级别的数据库,这是一个以学习为目的的项目。有许多特性,但是缺少…...
【密评】商用密码应用安全性评估从业人员考核题库(三)
商用密码应用安全性评估从业人员考核题库(三) 国密局给的参考题库5000道只是基础题,后续更新完5000还会继续更其他高质量题库,持续学习,共同进步。 501 多项选择题 《个人信息保护法》要求个人信息处理者应当采取哪些…...
MySQL进阶_查询优化和索引优化
文章目录 第一节、索引失效案例1.1 数据准备1.2 全值匹配我最爱1.3 最佳左前缀法则 第一节、索引失效案例 可以从以下维度对数据库进行优化: 索引失效、没有充分利用到索引–索引建立关联查询太多JOIN (设计缺陷或不得已的需求)–SQL优化服务器调优及各个参数设置…...
Hadoop2复安装过程详细步骤
1、在vmware中更改了虚拟机的网络类型,--->NAT方式,(虚拟交换机的ip可以从vmvare的edit-->vertual network editor看到) 2、根据这个交换机(网关)的地址,来设置我们的客户端windows7的ip&…...
【Java-LangChain:面向开发者的提示工程-7】文本扩展
第七章 文本扩展 扩展是将短文本(例如一组说明或主题列表)输入到大型语言模型中,让模型生成更长的文本(例如基于某个主题的电子邮件或论文)。这种应用是一把双刃剑,好处例如将大型语言模型用作头脑风暴的伙…...
竞赛 基于设深度学习的人脸性别年龄识别系统
文章目录 0 前言1 课题描述2 实现效果3 算法实现原理3.1 数据集3.2 深度学习识别算法3.3 特征提取主干网络3.4 总体实现流程 4 具体实现4.1 预训练数据格式4.2 部分实现代码 5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 基于深度学习机器视觉的…...
从技能需求到就业前景,了解前端和后端开发的优缺点和个人选择
文章目录 每日一句正能量一、引言前端开发后端开发 二、两者的对比分析三、技能转换和跨领域工作四:介绍全栈开发后记 每日一句正能量 命运决定的不是你的人生,能决定你人生的只有自己。 一、引言 前端和后端是Web开发中两个不可或缺的领域。前端开发主…...
Flutter笔记:AnimationMean、AnimationMax 和 AnimationMin 三个类的用法
Flutter笔记 AnimationMean、AnimationMax 和 AnimationMin三个类的用法 作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 :291148484163.com 本文地址:https://blog.csdn.net/qq_28550263/…...
华为云云耀云服务器L实例评测|云耀云服务器L实例部署Gogs服务器
华为云云耀云服务器L实例评测|云耀云服务器L实例部署Gogs服务器 一、云耀云服务器L实例介绍1.1 云耀云服务器L实例简介1.2 云耀云服务器L实例特点 二、Gogs介绍2.1 Gogs简介2.2 Gogs特点 三、本次实践介绍3.1 本次实践简介3.2 本次环境规划 四、远程登录华为云云耀云…...
操作系统--分页存储管理
一、概念介绍 分页存储:一是分内存地址,二是分逻辑地址。 1.分内存地址 将内存空间分为一个个大小相等的分区。比如,每个分区4KB。 每个分区就是一个“页框”,每个页框有个编号,即“页框号”,“页框号”…...
【算法练习Day10】有效的括号删除字符串中的所有相邻重复项逆波兰表达式求值
📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:练题 🎯长路漫漫浩浩,万事皆有期待 文章目录 有效的括号删除字符串中的所…...
10.1 校招 实习 内推 面经
绿泡*泡: neituijunsir 交流裙 ,内推/实习/校招汇总表格 1、自动驾驶一周资讯 - 苹果汽车项目泡汤?纵目科技IPO终止,腾讯与岚图汽车合作升级,158亿元现金收购比亚迪“史上最大”并购案 自动驾驶一周资讯 - 苹果汽车…...
React第五十七节 Router中RouterProvider使用详解及注意事项
前言 在 React Router v6.4 中,RouterProvider 是一个核心组件,用于提供基于数据路由(data routers)的新型路由方案。 它替代了传统的 <BrowserRouter>,支持更强大的数据加载和操作功能(如 loader 和…...
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
【解密LSTM、GRU如何解决传统RNN梯度消失问题】
解密LSTM与GRU:如何让RNN变得更聪明? 在深度学习的世界里,循环神经网络(RNN)以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而,传统RNN存在的一个严重问题——梯度消失&#…...
2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...
html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...
水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关
在水泥厂的生产流程中,工业自动化网关起着至关重要的作用,尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关,为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多,其中不少设备采用Devicenet协议。Devicen…...
面试高频问题
文章目录 🚀 消息队列核心技术揭秘:从入门到秒杀面试官1️⃣ Kafka为何能"吞云吐雾"?性能背后的秘密1.1 顺序写入与零拷贝:性能的双引擎1.2 分区并行:数据的"八车道高速公路"1.3 页缓存与批量处理…...
书籍“之“字形打印矩阵(8)0609
题目 给定一个矩阵matrix,按照"之"字形的方式打印这个矩阵,例如: 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为:1,…...
