【Linux学习】多线程——线程控制 | 线程TCB
🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!
线程控制 | 线程TCB
- 🧰线程控制
- 🎴线程创建
- 🎴线程结束
- 🎴线程等待
- 线程返回值
- 线程取消(线程结束的一种方式)
- 🎴线程分离
- 🧰C++多线程
- 🧰线程库中的TCB
- 🎴线程tid
- 🎴线程局部存储(__thread)
- 🧰总结
🧰线程控制
Linux内核中并不存在线程的概念,我们程序员是通过库来使用线程的,这个库是POSIX线程库,是由原生线程库提供的,它遵守POSIX标准,就像之前学过的System V标准一样。POSIX线程库有以下几个特点:
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
- 要使用这些函数库,要通过引入头文<pthread.h>。
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
🎴线程创建
系统调用接口:

- pthread_t* thread:输出型参数,将线程的tid值放入到我们外部创建好的pthread_t 类型的变量中。
- 第二个参数:用来设置线程属性,一般情况下设置成nullptr,等用到的时候再详细讲解。
- void* (*start_routine)(void *):函数指针,这是一个回调函数,该函数的内容就是新线程要执行的。
- void* arg:回到函数的参数。
- 返回值: 线程创建成功返回0,不成功返回错误码。
一般情况下,新线程的创建是不会失败的,万一失败了,也不会设置errno,因为errno是一个全局变量,某个线程改变了这个变量会对其他线程造成影响,所以直接将错误码返回即可。
- 在编译的时候,必须指定线程库,使用-l pthread选项。
接下来用这个接口创建一批线程:
#define NUM 10void* start_routine(void* args)
{sleep(1);string name = (char*)(args);while(1){cout<<"new thread name: "<<name<<endl;sleep(1);}
}int main()
{//创建一批线程for(size_t i = 0; i < NUM; ++i){pthread_t tid;char buffer[64];snprintf(buffer,sizeof buffer,"thread %d",i+1);pthread_create(&tid,nullptr,start_routine,(void*)buffer);}while(1){cout<<"----create success----"<<endl;sleep(1);}return 0;
}
创建10个线程,让它们同时运行,并且给每个线程编号,新线程死循环打印各自的线程名字,新线程在延时1秒后开始执行。

将上诉代码运行起来后,查看线程,可以看到一共有11个线程,其中1个主线程,10个新线程。

但是运行结果中,10个线程都是线程10,其他9个线程并没有出现,这是什么原因呢?

- 新线程中首先要延时1秒钟,然后才开始执行代码,在它延时的过程中,主线程一直在跑。
- 主线程中的名字缓冲区会被覆盖,最终只有"thread 10"。
- 当10个新线程开始执行时,需要去缓冲区中拿数据(缓冲区所有线程共享),所以拿到的都是"thread 10"。
上面代码中,本喵故意给新线程先延时了一秒钟,让主线程先跑,去覆盖缓冲区,如果不延时也有可能会出现上诉情况。
- 主线程和新线程到底谁先执行是不确定的,是由操作系统的调度器决定的。
即使不给新线程延时,也有可能是主线程先运行,在时间片结束之前,同样会完成数据覆盖,导致新线程从缓冲区中只能读到最终的数据。
所以说,上面的代码是有问题的,我们需要保证每个线程都有自己独一无二的缓冲区。
class ThreadData
{
public:pthread_t _tid;char _name[64];
};
创建一个类,这个类中包括线程的tid以及名字的缓冲区。

- 每个线程都在堆区new一个对象,来存放该线程的tid以及名字,然后将这个对象的地址传给新线程。
- 新线程通过主线程传过来的指针找到属于它的结构体对象,然后使用里面的数据。

此时10个线程就都能正常运行了,不存在缓冲区的覆盖问题了,因为一个线程有一个缓冲区。
🎴线程结束
- return nullptr结束线程

当新线程执行到return的时候,就会结束。
- 在线程中加了计数值,5秒后跳出循环,执行return,结束线程。

当计数值到了以后,新线程全部结束,只剩下主线程在执行。
- pthread_exit()结束线程
POSIX线程库专门提供了一个接口来结束线程:

- 参数:返回线程结束信息,当前阶段设置成nullptr即可。
调用该接口的线程会结束。

同样,当计数值到了以后,新线程会调用该接口,然后就只剩下主线程了,新线程全部结束了。
注意:
不能使用exit()来结束线程,因为exit系统调用是争对进程的,调用该接口会让整个进程都结束掉。
🎴线程等待
和进程一样,线程也是需要等待的,如果不等待会造成内存泄漏,也就是结束掉的线程PCB不会被回收(类似僵尸进程),但是我们看不到没有回收的现象。
系统调用:

- pthread_t thread:要等待的线程tid。
- void** retval:线程结束信息返回,这是一个输出型参数。
- 返回值:等待成功返回0,等待失败返回错误码。

主线程中并没有延时,它执行的速度是很快的。在新线程中需要进行计数,所以执行速度会慢很多。

可以看到,主线程在执行到线程等待的时候,会阻塞等待,不再往下执行,直到所有线程都等待成功才会继续向下执行。
所以说,线程等待是阻塞式等待。
线程返回值
线程等待和进程等待一样,主要有两个作用:
- 获取线程退出信息。
- 回收线程PCB资源,防止内存泄漏。
上面线程等待的代码中并没有获取线程退出的相关信息,那么该如何获取线程退出的相关信息呢?

- 新线程在结束的时候会返回一个void*类型的指针。
- 在pthread线程库中,有一个void类型的指针变量来接收从线程中返回的void指针。
- 指针变量和指针是有区别的,指针变量会开辟空间,里面存放的是指针。
- 指针就是地址,是数字,不会开辟空间。
如上图中代码所示,将整形数字10强转成void*类型,然后返回。
- 现在面临的问题就是怎么从pthread线程库中拿到从线程中返回的void*指针。

在主线程的栈区中有一个void类型的指针变量,新线程中返回的void类型指针最终会放到这个ret中。
- pthread线程库中有一个void** 类型的二级指针变量retval。
- pthread_join()系统调用将主线程中void*类型的指针变量的地址传给了pthread线程库中的二级指针变量,此时主线程就和线程库建立了联系。
- 将新线程中返回到线程库中的void*指针变量中的返回值,通过这种联系放到主线程中指针变量中----也就是 *retval = ret。
这样,我们就可以成功的获取到新线程退出时的返回信息了,桥梁就是pthread_join()系统调用。
pthread_join()系统调用中,之所以传的是二级指针,是为了在pthread库中能够找到主线程中一级指针变量 void * ret。

在线程等待时,传入ret的二级指针获取线程退出信息。
- 由于Linux中void* ret是8个字节,接收到的线程退出信息10也是一个void*类型的。
- 我们要想看到这个值,需要将它转换成整数,所以必须转成longlong类型,也是8个字节,如果转成int的话会有精度损失从而会报错。

可以看到,每个线程在退出时的退出信息都被主线程接收到了,由于所有线程的退出信息都是10,所以接收到的也都是10。

通过pthread_exit()同样可以将线程的退出信息返回到pthread的线程库中,然后再通过线程等待接口拿走这个退出信息。
- 在结构体中增加一个线程编号信息,每创建成功一个线程都给它一个编号。
- 新线程在退出的时候返回各自的编号。
- 线程等待代码不变,和上面一样。

此时我们就成功获得了各个线程在退出时候返回的编号,也就是获得了线程的退出信息。
整数都可以返回,更别说一个真正的地址了,可以将要返回的信息放在数组中,然后返回数组地址。
- 在学习进程等待的时候,我们不仅可以获得进程的退出信息,还能获得进程的退出信号,但是在线程退出时就没有获得线程退出信号,这是为什么呢?
- 因为信号是发给进程的,整个进程都会被退出,线程要退出信号也没有意义了。
- 而且pthread_join默认是能够等待成功的,并不考虑异常的问题,异常是进程要考虑的事,线程不用考虑。
线程取消(线程结束的一种方式)
线程取消的接口:

- 参数:要取消的线程tid。
- 返回值:取消成功返回0,失败返回错误码。
- 只有运行起来的线程才能被取消。

在主线程中,新线程被创建后,取消一半的线程,然后继续进行线程等待。

10个线程被创建后就会跑起来。
- 前五个线程被取消了,线程等待直接成功,不用再阻塞,被取消的线程等待成功后的返回值是-1,并不是我们设定的线程编号。
- 未被取消的后五个线程,仍然阻塞等待,等待成功后返回的是我们设定的线程编号。
所以说,如果一个线程是被取消结束的,它的退出码就是-1。它其实是一个宏定义:PTHREAD_CANCELED。
线程取消也是一种线程结束的方式,放在这里是为了能够通过线程等待看线程退出的退出码。
🎴线程分离
线程的tid也可以通过接口获得,就像获得pid一样,获取tid的接口:

这个接口也是POSIX线程库提供的,哪个线程调用该接口就会返回哪个线程的tid。
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
但是这样主线程就需要阻塞式等待线程的释放,主线程什么都干不了。能不能像进程那样不需要阻塞式等待(将SIGCHID信号设置为忽略),等新线程结束以后自动释放呢?
- 尤其是不需要关心线程返回值的时候,join是一种负担。
当然可以,将需要自动释放的线程设置成分离状态,将线程设置成分离状态意味着不需要主线程再关心该线程的状态,它会自动释放。
- joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
线程分离的接口:

- 参数:要分离的线程tid。
- 返回值:成功返回0,不成功返回错误码。
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。自己分离自己就需要使用接口获取到自己的tid。
线程分离后,如果主线程仍然等待该线程,就会等待失败,返回错误码。
新线程中分离自己:
void* start_routine(void* args)
{string name = static_cast<const char*>(args);size_t cnt = 5;pthread_detach(pthread_self());//线程分离while(cnt--){cout<<"new thread name: "<<name<<", cnt: "<<cnt<<endl;sleep(1);}pthread_exit(nullptr);
}int main()
{//创建新线程pthread_t tid;pthread_create(&tid,nullptr,start_routine,(void*)"thread one");cout<<"main thread tid: 0x"<<(void*)pthread_self()<<endl;//线程等待int n = pthread_join(tid,nullptr);cout<<"error: "<<n<<"->"<<strerror(n)<<endl;return 0;
}
在新线程中分离线程。

不是说线程分离了再进行线程等待就会失败吗?怎么上面的运行结果仍然是等待成功呢?
- 因为主线程先被调度,在新线程被创建但是没有执行的时候主线程就开始等待新线程了。
所以当新线程将自己分离以后,主线程已经处于等待状态了,它不认为新线程被分离,还会继续等待,而且可以等待成功。

可以让主线程延时一段时间,保证新线程先执行,也就是保证线程分离发生在线程等待之前。

可以看到,此时主线程在进行线程等待的时候就会失败,而且返回错误码。
在主线程中分离新线程:
最为稳妥的办法就是在主线程中分离新线程:

在主线程中分离新线程,任何进行线程等待,并且主线程在一直运行。

- 主线程等待新线程失败后直接返回错误码,然后接着向下运行,并不会阻塞。
在新线程运行结束以后,自动回收其PCB资源,只剩下主线程在运行。
- 一个线程一旦被分离就不用再管这个线程了,在它运行结束的时候系统会自动回收,不会造成内存泄漏。
🧰C++多线程
我们知道,C++也是可以多线程编程的,而且提供了多线程的库,而无论什么编程语言,什么库,在Linux系统上的多线程本质上都是对pthread原生线程库的封装。
接下面本喵就模拟一下C++对线程库的封装,写一个小组件,同时也方便我们后面直接使用:
#define NUM 1024class Thread;//前置声明class Context//线程上下文
{
public:Context():_this(nullptr),_args(nullptr){}//成员变量Thread* _this;void* _args;
};class Thread
{
public://重命名函数对象typedef std::function<void*(void*)> func_t;//构造函数//传入新线程执行的函数,参数,以及新线程编号Thread(func_t func, void* args = nullptr, int number = 0):_func(func),_args(args){//格式化线程名char buffer[NUM];snprintf(buffer,sizeof (buffer),"thread-%d",number);_name = buffer;//创建线程Context* ctx = new Context();ctx->_this = this;ctx->_args = _args;int n = pthread_create(&_tid,nullptr,start_routine,ctx);}void* run(void* args){return _func(args);}//由于调用成员函数有隐藏的this指针,所以使用static修饰//void* start_routine(this, void* args)static void* start_routine(void* args){Context* ctx = static_cast<Context*>(args);//安全的类型转换void* ret = ctx->_this->run(ctx->_args);delete ctx;return ret;}//线程等待void join(){int n = pthread_join(_tid,nullptr);assert(n==0);(void)n;}
private:std::string _name;func_t _func;void* _args;pthread_t _tid;
};

- 在调用start_routine成员函数的时候,会隐藏一个this指针。
- 而pthread_create中的函数指针只有一个形参,为了消除这个指针,用static修饰新线程调用的函数。
此时就面临一个新的问题,在static函数内,需要调用类内的成员函数run(),但是没有this指针无法调用。
- 创建一个上下文类,里面放线程类的this指针,在static函数内通过这个指针来调用类内的成员函数run()。
测试代码:
void* thread_run(void* args)
{string work_type=static_cast<const char*>(args);while(1){cout<<"新线程:"<<work_type<<endl;sleep(1);}
}int main()
{unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"thread1",1));unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"thread2",2));unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"thread3",3));thread1->join();thread2->join();thread3->join();return 0;
}

可以看到,成功创建3个新线程,并且在不停运行。
这里仅是语言层面对线程库的封装。
🧰线程库中的TCB
🎴线程tid
前面多次见过线程的tid值,但是一直不知道它是什么,现在来揭开它的神秘面纱。

新线程和主线成都打印新线程的tid,并且主线程也打印自己的tid。

- 主线程和新线程打印的新线程tid的值都是一样的。
- 而且tid的值是一个地址。

我们知道,Linux内核中是没有线程概念的,也没有对应的TCB结构。
- 用户创建线程时使用的是POSIX线程库提供的接口。
- 线程库中会调用clone()系统调用接口,在内核中创建线程复用的PCB结构。
- 这些轻量级进程共用一个进程地址空间。
系统中肯定不只一个线程存在,大量的线程势必要管理起来,管理的方式同样是先描述再组织。既然Linux内核中只有轻量级进程的PCB,那么描述线程的TCB结构就只能存在于线程库中。
所以pthread线程库中就会维护很多TCB结构:
//伪代码
struct pthread
{//线程局部存储//线程栈//....
}
线程库中的TCB里,存放着线程的属性,这里的TCB被叫做用户级线程。
- Linux线程方案:用户级线程以及用户关心的线程属性在线程库中,内核提供线程执行流的调度。
- Linux 用户级线程 : 内核轻量级进程= 1 :1
一个线程的所有属性描述是由两部组成的,一部分就是在pthread线程库中的用户级线程,另一部分就是Linux中的轻量级进程,它们俩的比例大约是1比1。

- pthread线程库从磁盘上加载到内存中后,通过页表再将虚拟地址空间和物理地址映射起来。
- 线程库最终是映射在虚拟地址空间中的共享区中的mmap区域。
既然线程库是映射在共享区的,那么线程库所维护的TCB结构也就一定在共享区。

如上图所示,将映射到共享区的动态线程库放大。
- 线程库中存在多个TCB结构来描述线程。
- 每个TCB的地址就是线程id。
线程tid的本质就是虚拟地址共享区中TCB结构体的地址。
- 线程的栈也在共享区中,而不在栈中。
- 虚拟地址空间中的栈是主线程的栈,共享区中动态库中的栈是新线程的栈。
所以说,线程的栈结构是相互独立的,因为存在于不同的TCB中(主线程除外)。
🎴线程局部存储(__thread)
在共享区线程库中的TCB里,有一个线程的局部存储属性,它是一个介于全局变量和局部变量之间线程特有的属性。

在主线程和新现在中同时打印全局变量g_val以及它的地址。

主线程和新线程打印的值都是一样的。
- 说明主线程和新线程共用一个全局变量。
那如果此时新线程仍然想用这个变量名,但是又不想影响其他线程,也就是让这个全局变量独立出来,该怎么办呢?此时就可以使用线程的局部存储属性了。

- 在全局变量g_val前面加__thread(两个下划线),此时这个全局变量就具有了局部存储的属性。
主线程和新线程同样打印这个全局变量,并且新线程将这个具有局部存储属性的全局变量不断加一。

- 主线程和新线程打印出来的全局变量的地址不相同了,说明此时用的并不是同一个全局变量。
- 新线程修改这个值,主线程不受影响。
- 可以将全局变量或者static变量添加 __thread,设置位线程局部存储。
- 此时每个线程的TCB中都会有一份该变量,相互独立,并不会互相影响。
🧰总结
有了进程的基础,线程有些地方可以进行类比,还是比较容易理解的。线程控制非常重要,而且在编程中经常使用到。
相关文章:
【Linux学习】多线程——线程控制 | 线程TCB
🐱作者:一只大喵咪1201 🐱专栏:《Linux学习》 🔥格言:你只管努力,剩下的交给时间! 线程控制 | 线程TCB 🧰线程控制🎴线程创建🎴线程结束…...
Node 10 接口
接口 简介 接口是什么 接口是 前后端通信的桥梁 简单理解:一个接口就是 服务中的一个路由规则 ,根据请求响应结果 接口的英文单词是 API (Application Program Interface),所以有时也称之为 API 接口 这里的接口指的是『数据接口』&#…...
大型互联网企业大流量高并发电商领域核心项目已上线(完整流程+项目白皮书)
说在前面的话 面对近年来网络的飞速发展,大家已经都习惯了网络购物,从而出现了一些衍生品例如:某宝/某东/拼夕夕等大型网站以及购物APP~ 并且从而导致很多大型互联网企业以及中小厂都需要有完整的项目经验,以及优秀处理超大流量…...
汇编语言学习笔记六
flag 寄存器 CF:进位标志位,产生进位CF1,否则为0 PF:奇偶位,如010101b,则该数的1有3个,则PF0,如果该数的1的个数为偶数,则PF1。0也是偶数 ZF:在相关指令执行后(运算和逻辑指令,传送指…...
多商户商城系统-v2.2.3版本发布
likeshop多商户商城系统-v2.2.3版本发布了!主要更新内容如下 新增 1.用户端退出账号功能 优化 1.平台添加营业执照保存异常问题 2.平台端分销商品优化-只显示参与分销的商品 3.优化订单详情显示营销价格标签 4.平台交易设置增加默认值 5.种草社区评论调整&a…...
科研人必看入门攻略(收藏版)
来源:投稿 作者:小灰灰 编辑:学姐 本文主要以如何做科研,日常内功修炼,常见科研误区,整理日常‘好论文’四个部分做以介绍,方便刚入门的科研者进行很好的规划。 1.如何做科研 1.1 选方向 当我…...
第5章 循环和关系表达式
1. strcmp()//比较字符串数组是否相等| string 可以直接用“”来判断 char word[5] "aaaa"; strcmp(word,"aaab");//相同输出0,不同输出1; 2. 延时函数 #include<ctime>float sec 2.3;long delay sec*CLOCKS_PER_SEC;long start c…...
Scalable Vector Graphics (SVG)中的svg、clipPath、mask元素
Scalable Vector Graphics (SVG)是一种用于描述二维向量图形的XML基础标记语言。使用SVG可以实现丰富的图形效果,而不需要像使用位图那样考虑分辨率和像素密度的问题,可以在不同设备上展示出相同的高质量图像。 在SVG中,除了基本形状如circl…...
Java基础(十五)集合框架
1. 集合框架概述 1.1 生活中的容器 1.2 数组的特点与弊端 一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。另一方面,使用数组存储对象方面具有一些弊端,而Java 集合…...
安装gitea
1、安装包(gitea-1.13.1-linux-amd64)上传到服务器,并添加执行权限 链接:https://pan.baidu.com/s/1SAxko0RhVmmD21Ev_m5JFg 提取码:ft07 chmod x gitea-1.13.1-linux-amd64 2、执行 ./gitea-1.13.1-linux-amd64 web…...
Java异常处理传递规范总结
java 异常分类 Thorwable类(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别…...
2d俯视视角游戏,可以切换多种枪械
文章目录 一、 介绍二、 人物移动、鼠标控制转向三、子弹脚本四、子弹随机抛壳五、 爆炸特效六、 发射子弹七、 子弹、弹壳对象池八、 散弹枪九、 火箭弹、发射火箭十、 下载工程文件 一、 介绍 2d俯视视角游戏。 人物视角跟随鼠标移动 多种枪械 抛壳效果 多种设计效果 对象池…...
大四的告诫
保研/考研方向就绩点,(各种)比赛,(考研)刷题为主 工作就算法(比赛),项目,实习为主 👂 LOCK OUT - $atori Zoom/KALONO - 单曲 - 网易云音乐 &…...
滚珠螺杆在设备上的应用
滚珠螺杆跟直线导轨一样,是很多机械设备上不可或缺的重要部件,它是确保机器能够具备高加工精度的前提条件,因此本身对于精度的要求也相当地高。今天,我们就来了解一下滚珠螺杆在不同设备上的应用吧! 1、大型的加工中心…...
Day41线程同步
线程同步 案例:三个窗口卖100张票 //定义一个类SellTicket实现Runnable接口,定义成员变量100张票 public class SellTicket implements Runnable{private int tickets 100;//重写run方法Overridepublic void run(){while (true){ //没有票后&…...
设计模式之享元模式
参考资料 曾探《JavaScript设计模式与开发实践》;「设计模式 JavaScript 描述」享元模式设计模式之享元模式Javascript 设计模式 - 享元模式 定义 享元模式的英文叫:Flyweight Design Pattern。享元设计模式是用于性能优化的模式,这种设计…...
【GAMES101】05 Rasterization(Triangles)
光栅化过程:将一系列变换后的三角形转换为像素的过程。 三角形在图形学中得到很多的应用。 最基础的多边形(边数最少)。任何多边形都可以拆成三角形。性质:三角形内部一定是平面的。三角形内外部定义非常清楚。定义三个顶点后&a…...
13. Pod 从入门到深入理解(二)
本章讲解知识点 Pod 容器共享 VolumeConfigMapSecretDownward APIEmptyDir VolumeHostPath Volume1. Pod 容器共享 Volume 1.1. Volume 的背景及需要解决的问题 存储是必不可少的,对于服务运行产生的日志、数据,必须有一个地方进行保存,但是我们的容器每一次重启都是“恢复…...
ORBBEC(奥比中光)AstraPro相机在ROS2下的标定与D2C(标定与配准)
文章目录 1.rgb、depth相机标定矫正1.1.标定rgb相机1.2.标定depth相机1.3.rgb、depth相机一起标定(效果重复了,但是推荐使用)1.4.取得标定结果1.4.1.得到的标定结果的意义 1.5.IR、RGB相机分别应用标定结果1.5.1.openCV应用标定结果1.5.2.ros…...
常量与变量:编程中重要的两种数据类型
常量与变量 在编程中,我们常常需要存储一些数据。这些数据有些是恒定不变的,有些却是可以随时变化的。对于恒定不变的数据,我们称之为常量;对于可以变化的数据,我们则称之为变量。这两种数据类型在程序中非常重要&…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
css的定位(position)详解:相对定位 绝对定位 固定定位
在 CSS 中,元素的定位通过 position 属性控制,共有 5 种定位模式:static(静态定位)、relative(相对定位)、absolute(绝对定位)、fixed(固定定位)和…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...
招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...

