第2章 线程管理
2.1 线程的基本操作
每个程序至少有一个执行main()函数的线程, 其他线程与主线程同时运行。 如main()函数执行完成退出一样, 线程执行完函数也会退出。 为线程创建std::thread() 对象后, 需要等待该线程结束。 那么, 先启动线程。
2.1.1 启动线程
一些情况下, 任务函数对象需要通过某种通讯机制进行参数的传递, 或者执行一系列独立操作, 通过通讯机制传递信号让线程信号停止。 先放下这些特殊情况不谈, 简单来说, 使用C++线程库启动线程, 就是构造std::thread对象:
void do_some_work();
std::thread my_thread(do_some_work);
这里需要包含头文件, std::thread可以通过有函数操作符类型的实例进行构造:
class background_task
{
public:void operator()() const{do_something();do_something_else();}
};background_task f;
std::thread my_thread(f);
代码中, 提供的函数对象会复制到新线程的存储空间中, 函数对象的执行和调用都在线程的内存空间中进行。
注意:当把函数对象传入线程构造函数中时,如果你传递了一个临时变量, 而不是一个命名的变量。 C++编译器会将其解析为函数声明, 而不是雷翔对象的定义。
std::thread my_thread(background_task());
这相当对声明了一个名为my_thread的函数, 这个函数带有一个函数(函数指针执行没有参数并返回background_task对象的函数), 返回一个std::thread对象的函数。
使用在前面命名函数对象的方式, 或使用多组括号(1), 或者使用统一的初始化语法(2), 都可以避免该问题。
std::thread my_thread((background_task())); //(1)
std::thread my_thread{background_task()}; //(2)
Lambda表达式也能避免这个问题。 Lambda表达式允许使用一个可以捕获局部变量的局部函数(可以避免传递参数)。 之前的例子可以改写为Lambda表达式的方式:
std::thread my_thread([]{do_something();do_something_else();});
线程启动后是要等待线程结束, 还是让其自动运行。 当std::thread()对象销毁之前还没有做出决定, 程序就会终止(std::thread的析构函数会调用std::terminate())。因此, 即使是有异常存在, 也需要确保线程能够正确汇入(joined)或分离(detached)。
如果不等待线程汇入, 就必须保证线程结束之前, 访问数据的有效性。这不是一个新问题–单线程代码中, 对象销毁之后再去访问, 会产生未定义行为–不过, 线程的生命周期增加了这个问题发生的几率。
这种情况很可能发生在线程还没借宿, 函数已经退出的时候, 这时线程函数还持有函数局部变量的指针或引用。
代码2.1 函数已经返回, 线程依旧访问局部变量
struct func
{int& i;func(int& i_) : i(i_) {}void operator() (){for (unsigned j=0 ; j<1000000 ; ++j){do_something(i); // (1) 潜在访问隐患:空引用(dangling reference)}}
};void oops()
{int some_local_state=0;func my_func(some_local_state);std::thread my_thread(my_func);my_thread.detach(); // (2) 不等待线程结束
} // (3) 新线程可能还在运行
代码中, 已经决定不等待线程(使用了detach()(2)), 所以当oops()函数执行完成时(3), 线程中的函数可能还在运行。 如果线程还在运行, 就会去调用do_something(i)(1), 这时就会访问已经销毁的变量。 如同一个单线程程序–允许在函数完成后继续拥有局部变量的指针或引用。 当然, 这种情况发生时, 错误并不明显, 会使多线程更容易出错。 运行顺序参考下表2.1。
表2.1 分离( leaving it
to run)线程在局部变量销毁后, 仍对该变量进行访问。
| 主线程 | 新线程 |
|---|---|
| 使用some_local_state构造my_func | |
| 开启新线程my_thread | |
| 启动 | |
| 调用func::operator() | |
| 将my_thread分离 | 执行func::operator();可能会在do_something中调用some_local_state的引用 |
| 销毁some_local_state | 持续运行 |
| 退出oops函数 | 持续执行func::operator();可能会在do_something中调用some_local_state的引用 --> 导致未定义行为 |
| 这种情况的常规处理方法:将数据复制到线程中。 如果使用一个可调用的对象作为线程函数, 这个对象就会复制到线程中, 而后原始对象会立即销毁。 如代码2.1所示, 但对于对象中包含的指针和引用还需谨慎。 使用访问局部变量的函数去创建线程是一个糟糕的注意(bad idea)。 | |
| 此外, 可以通过join()函数来确保线程在主函数完成前结束。 |
2.1.2 等待线程完成
如需等待线程, 需要使用join()。 将代码2.1的my_thread.detach()替换为my_thread.join(), 就可以确保局部变量在线程完成后才销毁。 因为主线程并没有做什么事, 使用独立的线程去执行函数变得意义不大。 但在实际中, 原始线程要么有自己的工作要做, 要么会启动多个子线程来做一些有用的工作, 并等待这些线程结束。
当你需要对等待中的线程有更灵活的控制时, 如: 看一下某个线程是否结束, 或者只等待一段时间(超过时间就判定为超时)。 想要做到这些, 需要使用其他机制来完成, 比如条件变量和future。 调用join(), 还可以清理了线程相关的内存, 这项std::thread对象将不再与已完成的线程有任何关联。 这意味着, 只能对一个线程使用一次join(), 一旦使用过join(), std::thread对象就不能再次汇入了。 当对使用joinable()时, 将返回flase。
2.1.3 特殊情况下的等待
如果想要分离线程, 可以在线程启动后, 直接使用detach()进行分离。 如果等待线程, 则需要细心挑选使用join()的位置。 当在线程运行后产生的一场, 会在join()调用之前抛出, 这样就会跳过join()。
避免应用被抛出的异常所终止。 通常, 在无异常的情况下使用join()时, 需要在异常处理过程中调用join(), 从而避免生命周期的问题。
代码2.2 等待线程完成
struct func; // 定义在代码2.1中
void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);try{do_something_in_current_thread();}catch(...){t.join(); // (1)throw;}t.join(); // (2)
}
代码2.2中使用了try/catch块确保线程退出后函数才结束。 当函数正常退出后, 会执行(2)处。 当执行过程中抛出异常, 程序会执行到(1)。 如果线程在函数之前结束–就要查看是否因为线程函数使用了局部变量的引用–而后确定一下程序可能会退出的途径。有一个叫RAII(Resource Acquisition Is Initialization)的机制可以解决这个问题,提供一个类, 在析构函数中使用join()。
代码2.3 使用RAII等待线程完成
class thread_guard
{std::thread& t;
public:explicit thread_guard(std::thread& t_):t(t_){}~thread_guard(){if(t.joinable()) // (1){t.join(); // (2)}}thread_guard(thread_guard const&)=delete; // (3)thread_guard& operator=(thread_guard const&)=delete;
};struct func; // 定义在代码2.1中void f()
{int some_local_state=0;func my_func(some_local_state);std::thread t(my_func);thread_guard g(t);do_something_in_current_thread();
} // (4)
线程执行到(4)时,局部对象就要被逆序销毁了。 因此, thread_guard对象g是第一个被销毁的, 这时线程在析构函数中被加入(2)到原始线程中。 即使do_something_in_current_thread抛出一个异常, 这个销毁依然会发生。
在thread_guard析构函数的测试中, 首先判断线程是否可汇入(1)。如果可汇入, 会调用join()(2)进行汇入。拷贝构造函数和拷贝复制操作标记为=delete(3), 是为了不让编译器自动生成。 直接对对象进行拷贝或复制是很危险的, 因为这可能会弄对已汇入的线程。 通过删除生命, 任何尝试给thread_guard对象复制的操作都会引发一个编译错误。 参阅附录A的A.2节。
如果不想等待线程结束, 可以分离线程, 从而避免异常。 不过, 这就打破了线程与std::thread对象的联系, 即使线程仍然在后台运行着, 分离操作也能确保在std::thread对象销毁时不调用std::terminate()。
2.1.4 后台运行线程
使用detach()会让线程在后台运行, 这就意味着与主线程不能直接交互。 如果线程分离, 就不可能有std::thread对象使用它, 分离线程的确在后台运行, 所以分离的线程不能汇入。 不过C++运行库保证, 当线程退出时, 相关资源的能够正确回收。
分离线程通常为守护线程(daemon threads)。 UNIX中守护线程,是指没有任何显示的接口, 并在后台运行的线程, 这种线程的特点就是长时间运行。线程的生命周期可能会从应用的起始到结束, 可能会在后台监视文件系统, 还有可能对缓存进行清理, 或者对数据结构进行优化。另外, 分离线程只能确定线程什么时候结束, 发后即忘(fire and forget)的任务使用到就是分离线程。
如2.1.2节所示, 调用std::thread成员函数detach成员函数detach()来分离一个线程。 之后, 相应的std::thread对象与实际执行的线程无关, 并且这个线程也无法汇入:
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
为了从std::thread对象中分离线程, 不能对没有执行线程的std::thread对象使用detach(), 并且要使用同样的方式进行检查–当std::thread对象使用t.joinable()返回的是true, 就可以使用t.detach()。
试想如何能让一个文字处理应用同事编辑多个文档。 无论是用户界面, 还是在内部应用内部进行, 都有很多的解决方法。 虽然, 这些窗口看起来是完全独立的, 每个窗口都有自己独立的菜单选项, 但他们却运行在同一个应用实例中。 一种内部处理方式是, 让每个文档处理窗口拥有自己的线程。 每个线程运行同样的代码, 并个隔离不同窗口处理的数据。 如此这般, 打开一个文档就要启动一个新线程。 因为是对独立文档进行操作, 所以没有必要等待其他线程完成, 这里就可以让文档处理窗口运行在分离线程上。
代码2.4使用分离线程处理文档
void edit_document(std::string const& filename)
{open_document_and_display_gui(filename);while(!done_editing()){user_command cmd=get_user_input();if(cmd.type==open_new_document){std::string const new_name=get_filename_from_user();std::thread t(edit_document,new_name); // (1)t.detach(); // (2)}else{process_user_input(cmd);}}
}
如果用户选择打开一个新文档, 需要启动一个新线程去打开新文档(1), 并分离线程(2)。 与当前线程做出的操作一样, 新线程只不过是打开另一个文件而已。 所以, edit_document函数可以复用, 并通过传参的形式打开新的文件。
这个例子也展示了传参启动线程的方法: 不仅可以向std::thread构造函数(1)传递函数名, 还可以传递函数所需的参数(实参)。 当然, 也有其他方法可以完成这项功能, 比如: 使用带有数据的成员函数, 代替需要传参的普通函数。
2.2 传递参数
如代码2.4所示, 向可调用对象或函数传递参数很简单, 只需要将这些参数作为std::thread构造函数的附加函数即可。这些参数会拷贝至新线程的内存空间中(同临时变量一样)。即使函数中的参数是引用的形式,拷贝操作也会执行。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代码创建了一个调用(3, “hello”)的线程。 注意, 函数f需要一个std::string对象作为第二个参数, 但这里使用的是字符串的字面值, 也就是char const*类型, 线程的上下文完成字面值想std::string的转化。 需要特别注意, 指向动态变量的指针作为参数的情况, 代码如下:
void f(int i,std::string const& s);
void oops(int some_param)
{char buffer[1024]; //(1)sprintf(buffer, "%i",some_param);std::thread t(f,3,buffer); // (2)t.detach();
}
buffer(1)是一个指针变量, 指向局部变量, 然后此局部变量通过buffer传递到新线程中(2)。此时, 函数oops可能会在buffer转换成std::string之前结束, 从而导致未定义的行为。 因为, 无法保证隐式转换的操作和std::thread构造函数的拷贝操作的顺序, 有可能std::thread的构造函数拷贝的是转换前的变量(buffer指针)。解决方案就是在传递到std::thread构造函数之前, 就将字面值转化为std::string:
void f(int i,std::string const& s);
void not_oops(int some_param)
{char buffer[1024];sprintf(buffer,"%i",some_param);std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬空指针t.detach();
}
相反的情形(期望传递一个非常量引用, 但复制了整个对象)倒是不会出现, 因为会出现编译错误。 比如, 尝试使用线程更新引用传递的数据结构:
void update_data_for_widget(widget_id w, widget_data& data); // (1)
void oops_again(widget_id w)
{widget_data data;std::thread t(updata_data_for_widget, w, data); // (2)display_status();t.join();process_widget_data(data);
}
虽然updata_data_for_widget(1)的第二个参数期待传入一个引用, 但std::thread的构造函数(2)并不知晓, 构造函数无视函数参数类型, 盲目地拷贝已提供的变量。 不过, 内部代码会将拷贝的参数以右值的方式进行传递, 这是为了哪些支持移动的类型, 而后会尝试以右值为实参调用updata_data_for_widget。 但因为函数期望的是一个非常量引用作为参数(而非右值), 所以会在编译时出错。 对于熟悉std::bing的开发者来说, 问题的解决方法很简单:可以使用std::ref将参数转换成引用的形式。 因此可将线程的调用改为以下形式:
std::thread t(update_data_for_widget, w, std::ref(data));
这样update_data_for_widget就会收到data的引用, 而非data的拷贝副本, 这样代码就能顺利的通过编译了。
如果熟悉std::bind, 就应该不会对以上传参的语法感到陌生, 因为std::thread构造函数和std::bind的操作在标准库中以相同的机制进行定义。 比如, 你也可以传递一个成员函数指针作为线程函数, 并提供一个合适的对象指针作为第一个参数:
class X
{public:void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x); // 1
另一种有趣的情形是, 提供的参数仅支持移动(move),不能拷贝。 "移动"是指原始对象中的数据所有权转移给另一个对象, 从而这些数据就不再原始对象中保存(类似于剪切)。 std::unique_ptr就是这样一种类型, 这种类型为动态分配的对象提供内存自动管理机制(类似垃圾回收机制)。 同一时间内, 只允许一个std::unique_ptr实例指向一个对象, 并且当这个实例销毁时, 指向的对象也将被删除。 移动构造函数(move constructor)和移动赋值操作符(move assignment operator)允许一个对象的所有权后, 就会留下一个空指针。 使用移动操作可以将对象转换成函数可接受的实参类型, 或满足函数返回值类型要求。 当原对象是临时变量时, 则自动进行移动操作, 但当原对象是一个命名变量, 移动的时候就需啊哟使用std::move()进行显示移动。下面的代码展示了std::move的用法, 展示了std::move 是如何移动态对象的所有权到线程中去的:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(jprocess_big_object, std::move(p));
通过在std::thread构造函数中执行std::move§, big_object对象的所有权首先被转移到新创建线程的内部存储中, 之后再传递给process_big_object函数。
C++标准线程库中和std::unique_ptr在所属权上相似的类有好几种, std::thread为其中之一。 虽然, std::thread不像std::unique_ptr能占有动态对象的所有权, 但是它能占用其他资源: 每个实例都负责管理一个线程。 线程的所有权可以在多个std::thread实例中转移, 这依赖于std::thread实例的可移动且不可复制性。 不可复制性表示某一时间点, 一个std::thread实例只能关联一个执行线程。 可移动性是的开发者可以自己决定, 哪个实例拥有线程实际执行的所有权。
相关文章:
第2章 线程管理
2.1 线程的基本操作 每个程序至少有一个执行main()函数的线程, 其他线程与主线程同时运行。 如main()函数执行完成退出一样, 线程执行完函数也会退出。 为线程创建std::thread() 对象后, 需要等待该线程结束。 那么, 先启动线程。…...
机器学习第二十六周周报 ARIMA Clustering model
文章目录 week26 ARIMA & Clustering model摘要Abstract一、龙格库塔方法二、文献阅读1. 题目2. abstract3. 网络架构3.1 ARIMA模型3.2 K-means聚类3.3 评估标准 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程4.3.1 数据来源4.3.2 使用ARIMA模型预测4.3.3 K-means聚类…...
支持XP系统的最新firefox浏览器
都2024年了,还XP系统?事情是这样的,我要维护一下非常老的项目,系统部署在windows server 2003下面。升级系统是不太可能了。只能在老系统上维护,老的系统上自带的IE 6.0浏览器,在当前几乎是不可用状态&…...
Pytorch从零开始实战17
Pytorch从零开始实战——生成对抗网络入门 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——生成对抗网络入门环境准备模型定义开始训练总结 环境准备 本文基于Jupyter notebook,使用Python3.8,Pytorch1.8cpu…...
openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c
文章目录 openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c概述笔记END openssl3.2 - 官方demo学习 - signature - EVP_DSA_Signature_demo.c 概述 DSA签名(摘要算法SHA256), DSA验签(摘要算法SHA256) 签名 : 用发送者的私钥进行签名. 验签 : 用发送者的公…...
vue2使用 element表格展开功能渲染子表格
默认样式 修改后 样式2 <el-table :data"needDataFollow" border style"width: 100%"><el-table-column align"center" label"序号" type"index" width"80" /><el-table-column align"cent…...
一个简单的ETCD GUI工具
使用ETCD没有好用的GUI工具,随手用c#写了一个, 做得好玩的一个ETCD GUI工具,后面加上CLI 工具,类似于 redis Cli工具一样,简化在 Linux下面的操作,不知道有没有必要, git 地址如下,…...
vue2 使用pdf.js 实现pdf预览,并可复制文本
需求:pdf预览,并且可以选中pdf的内容进行复制。 在ruoyi的vue前端项目中用到,参考了网上不少文章,因为大部分没给具体的pdf.js版本,导致运行过程中报各种api 错误,经过尝试以下版本可用,…...
REPLACE INTO
简介 在数据库中,REPLACE INTO 是一种用于插入或更新数据的(DML) SQL 语句。它与 INSERT INTO 语句类似,但具有一些特殊的行为。 语法 REPLACE INTO table_name (column1, column2, ...) VALUES (value1, value2, ...); repla…...
idea 安装免费Ai工具 codeium
目录 概述 ide安装 使用 chat问答 自动写代码 除此外小功能 概述 这已经是我目前用的最好免费的Ai工具了,当然你要是有钱最好还是用点花钱的,比如copilot,他可以在idea全家桶包括vs,还有c/c的vs上运行,还贼强&am…...
关于C#中的Select与SelectMany方法
Select 将序列中的每个元素投影到新表单。 实例1 IEnumerable<int> squares Enumerable.Range(1, 10).Select(x > x * x);foreach (int num in squares) {Console.WriteLine(num); } /*This code produces the following output:149162536496481100 */ 实例2 str…...
CentOS上安装Mellanox OFED
打开Mellanox官网下载驱动 Linux InfiniBand Drivers 点击下载链接跳转至 Tgz解压缩执行 ./mlnxofedinstall发现缺少模块 # ./mlnxofedinstall Logs dir: /tmp/MLNX_OFED_LINUX.11337.logs General log file: /tmp/MLNX_OFED_LINUX.11337.logs/general.log Verifying KMP rpm…...
无/自监督去噪(1)——一个变迁:N2N→N2V→HQ-SSL
目录 1. 前沿2. N2N3. N2V——盲点网络(BSNs,Blind Spot Networks)开创者3.1. N2V实际是如何训练的? 4. HQ-SSL——认为N2V效率不够高4.1. HQ-SSL的理论架构4.1.1. 对卷积的改进4.1.2. 对下采样的改进4.1.3. 比N2V好在哪ÿ…...
【24.1.19】
24.1.19 本周工作内容下周工作计划 本周工作内容 本周的话主要的一个工作还是第三部分页面部分的完成工作,那就先来汇报一下第三部分的工作进度,第三部分的页面工作呢已经完成啦,就在刚刚提交啦全部的代码,那么这一部分的工作呢也…...
使用mamba替换conda和anaconda配置环境安装软件
使用mamba替换miniconda和anaconda,原因是速度更快,无论是创建新环境还是激活环境 conda、mamba、anaconda都是蟒蛇的意思… 下载mambaforge wget https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-Linux-x86_64.sh ba…...
鸿蒙开发系列教程(四)--ArkTS语言:基础知识
1、ArkTS语言介绍 ArkTS是HarmonyOS应用开发语言。它在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开…...
Pix2Pix理论与实战
本文为🔗365天深度学习训练营 中的学习记录博客 原作者:K同学啊|接辅导、项目定制 我的环境: 1.语言:python3.7 2.编译器:pycharm 3.深度学习框架Pytorch 1.8.0cu111 一、引入 在之前的学习中,我们知道…...
[GN] 后端接口已经写好 初次布局前端需要的操作(例)
提示:前端项目一定要先引入组件 配置。再编码!!!! 文章目录 使用 vue-cli 脚手架初始化前端工程化配置引入Vue前端组件库 -- arco前后端联调引入Md 编辑器组件 使用 vue-cli 脚手架初始化 使用安装脚手架工具…...
AIGC:人工智能驱动的数据分析新时代
AIGC:人工智能驱动的数据分析新时代 随着人工智能技术的迅猛发展,我们正迎来数据分析的新时代,其中AIGC(Artificial Intelligence with Generative Capabilities)的应用成为引领潮流的重要方向。本文将深入探讨几个关…...
Windows Qt C++ VTK 借助msys环境搭建
本示例仅仅是搭建环境,后续使用还得大佬指导。 Qt 6.6.0 MinGW 64bit 借助msys2 来安装VTK 包,把*.dll 链接进来,就可以用了。 先安装VTK 包。 Package: mingw-w64-x86_64-vtk - MSYS2 Packages 执行 pacman 命令:pacman -…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
NPOI操作EXCEL文件 ——CAD C# 二次开发
缺点:dll.版本容易加载错误。CAD加载插件时,没有加载所有类库。插件运行过程中用到某个类库,会从CAD的安装目录找,找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库,就用插件程序加载进…...
DBLP数据库是什么?
DBLP(Digital Bibliography & Library Project)Computer Science Bibliography是全球著名的计算机科学出版物的开放书目数据库。DBLP所收录的期刊和会议论文质量较高,数据库文献更新速度很快,很好地反映了国际计算机科学学术研…...
Modbus RTU与Modbus TCP详解指南
目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
