线程的概念和控制
自从20世纪60年代提出了进程的概念之后,操作系统一直以进程作为独立运行的基本单位。到了20世纪80年代,人们又提出了比进程更小的、能独立运行的基本单位——线程。提出线程的目的是试图提高系统并发执行的程度,从而进一步提高系统的吞吐量。现在,线程的概念已经得到了广泛的应用,现代操作系统都引入了线程的概念。
那么在我们的日常生活中,多线程在哪些地方被使用呢?例如网页浏览器中会有一个线程用于显示图像和文本,另一个线程用于从网络接收数据;文档处理器中会有一个线程用于显示文本,另一个线程用于读入用户的键盘输入,还有一个线程用于在后台进行拼写和语法检查等等。这些都是我们日常生活中的例子。说了这么多,那么线程到底是什么呢?我们继续往下深入了解吧。
线程的概念
如果说操作系统中引入进程的目的是使多个程序并发执行,改善资源的利用率,提高系统的吞吐量,那么在操作系统中再引入线程,则是为了减少程序并发执行时所付出的时间和空间开销,使操作系统具有更好的并发性。为了说明这一点,我们先回顾一下进程的2个基本属性,如下所述:
- 进程是一个可以拥有资源的独立单位。
- 进程是一个可以独立调度和分配的基本单位。
正是因为进程具有这2个基本属性,才使得进程成为一个能独立运行的基本单位,从而也就构成了进程并发执行的基础。
在CPU调度进程的时候,其实是在调度进程的PCB
,把进程的PCB
放在运行队列当中,然后依次执行队列当中的进程,那么如果我们想要一个进程被CPU多次运行,从而让一个进程有多个执行流,只需要创建更多的PCB就可以了。在之前学习的时候,我们认为一个进程只有一个PCB,这其实不完全对,一个进程可以有多个PCB结构体(只针对Linux)。我们把同一个进程的每一个PCB,都认为是一个线程。
线程是进程内的一个执行分支。线程的执行粒度要比进程细。
每个线程会拿到同一个进程的不同部分代码,从而让多个区域的代码一起执行,进而提高进程的效率。我们继续来看线程与进程之间的关系。
我们在学习进程的时候说过:进程=内核数据结构+代码和数据
。所以在上面的图片中,PCB、进程地址空间和页表
都属于进程的内核数据结构,这些内容加起来才叫做进程。
我们先来回顾一下进程的部分知识。为了使进程能并发执行,操作系统必须进行下面的一系列操作
- 创建进程。系统在创建进程时,必须为之分配其所必须的、除CPU以外的所有资源,如内存空间、I/O设备及建立相应的PCB。
- 撤销进程。系统在撤销进程时,又必须对分配给进程的资源进行回收操作,然后在撤销其PCB。
- 进程状态的切换。进程在建立以后到被撤销之前,要经历若干次进程的状态切换,在进行进程状态转换时,要保留进程执行的CPU环境,并设置新选中进程的CPU环境,为此系统需要处理花费处理机较多时间。
总而言之,由于进程是一个资源的拥有者,因此在进程的创建、撤销以及状态转换中,系统要为此付出较多的时间和空间开销。也正是因为如此,系统中所设置的进程数目不宜过多,进场切换的频率也不宜太高,但是这样就限制了进程并发程度的进一步提高。
于是,提高进程的并发程度,同时又能尽量减少系统的开销,成为操作系统设计者追求的目标。操作系统的设计者想到可以把进程的2个基本属性分开,由操作系统分别加以处理,即作为独立分配资源的单位,不再作为调度和分派的基本单位,在这样的思想指导下,产生了线程。
进程是承担系统资源分配的基本实体
因为多个线程共享地址空间,页表等进程资源,所以一个线程不可能承担系统的资源分配,反而来说,线程是被分配系统资源的。操作系统分配资源时,先以进程为基本单位分配给进程,然后进程在去分配给不同的线程。
线程是进程的一个实体,是被独立调度和分派的基本单位,表示进程中的一个控制点,执行一系列的指令。由于同一个进程内的多个线程都可以访问进程的所有资源,因此线程之间的通信要比进程之间的通信方便的多;同一进程内的线程切换也因为线程的轻装而方便的多。
线程的组成
线程有时候也被称为轻型进程(Light Weight Process,LWP)。每个线程有一个线程控制块(Thread Control Block,TCB),用于保存自己的私有信息,主要由以下几部分组成。
- 线程标识符,它是唯一的。
- 描述处理机状态信息的一组寄存器,包括通用寄存器、指令计数器、程序状态字等。
- 栈指针,每个线程有用户栈和内核栈两个栈,当线程在用户态下运行时使用自己的用户栈,当用户线程转到核心态下运行时使用内核栈。
- 一个私有存储区,存放现场保护信息和其它与该线程相关的统计信息。
线程必须在某个进程内执行,使用进程的其它资源,如程序、数据、打开的文件和信号量等。由于进程调度已经有一套非常完整的结构和体系了,所以线程不需要再额外设计一套属于线程的结构和体系,Linux中没有设计TCB这样的结构,而是直接复用进程的数据结构和管理算法来设计线程。
综上所述,在Linux中,一个进程可以有多个PCB,当PCB的数量为1时,那么这个PCB就代表线程;当PCB的数量大于1时,就把这些PCB叫做线程。所以Linux中没有真正意义上的线程,而是用“进程”(内核数据结构)模拟的线程。一个执行流由一个PCB维护,一个执行流可能表示进程,也可能表示线程。
页表
在学习进程的时候,我们谈过页表这个概念,是一种重要的数据结构,它是联系物理内存和虚拟内存之间的桥梁,用于实现虚拟内存到物理内存的映射。但是我们并没有深入了解页表这个概念,所以现在我们就来深入了解一下。
在现代操作系统中,为了更高效地管理内存并提供内存保护、多任务隔离等功能,引入了虚拟内存的概念。每个进程都有自己独立的虚拟地址空间,这个空间可以远大于实际的物理内存大小。而物理内存则是实际的内存硬件资源。
不管是用户级页表还是内核级页表,大家用的数据都是一样的,所以页表也要被操作系统管理起来,那么如何管理呢?答案就是先描述,再组织。页表中的每一个条目都是一个数据结构,把这些数据通过struct
管理起来。说了这么多,我们该如何看待地址空间和页表呢。
- 地址空间是进程能看到的资源窗口
- 页表决定进程真正拥有资源的情况
- 合理的对地址空间+页表进行资源划分,我们就可以对一个进程所有的资源进行分类。
页表的主要作用是将进程的虚拟地址转换为物理地址。当进程访问一个虚拟地址时,操作系统通过查询页表来确定该虚拟地址对应的物理地址。如果该虚拟地址对应的物理页不在内存中(可能因为页面置换被换出到磁盘等原因),就会触发缺页中断,操作系统会将所需的页面从磁盘加载到物理内存中,并更新页表。在Linux当中,不管是内存还是磁盘,都被划分了以4kb
为基本单位的数据块,一个数据块就被叫做页框或页帧。
那么虚拟地址是如何转换到物理地址的呢?我们以32位操作系统为例。页表的结构如下。
32位并不是按照整体进行计算的,而是以10+10+12=32
分成3部分。
第一部分是前10位,由页目录进行管理,前10位地址有 2 10 = 1024 2^{10}=1024 210=1024种可能,而页目录就是一个长度为1024的数组。处理地址时,会先通过前10位找到页目录中对应的下标位置,而每个页目录中的元素都指向一个页表。
第二部分是中间的10位,由页表进行管理,同样地址也是有1024种可能,处理地址时,会解析中间的10位,在页表中找到对应的下标,然后就能找到对应的内存。
第三部分是最后的12位,这12位正好和页框,页帧的大小一样,但是我们要访问的是某个字节,而不是4kb。所以你要访问的物理地址就是物理地址的起始地址处加上虚拟地址的最后12位(即你要访问物理内存在页框中的偏移量)。
总结:就是先查找虚拟地址的前10个比特位,到页目录中查找指定的页表,然后再拿10个比特位去页表中找到对应的位置,直接就找到了页框的物理起始地址,然后再拿最后12位充当偏移量,这样就可以通过虚拟地址转换成物理地址了。比如,在32位机器中,int
占4个字节,4个字节就有4个地址,那么我们平时对这个整型取地址只会出来一个地址,而这个地址就是起始地址,把这个地址住转换成物理地址中的起始地址,在根据整型连续取4个字节就可以把这个整型拿出来了。所以起始地址+类型=起始地址+偏移量
。
线程控制
POSIX线程库
我们在之前讲过内核中没有明确的线程概念,只有轻量级进程这个概念,因此是不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用。所以我们要想控制线程,我们使用的是pthread
线程库,它是在应用层上的,是对轻量进程接口进行封装,为用户提供直接线程的接口,让用户感觉到是直接对线程进行控制。
我们把这个线程库叫做原生线程库,是因为几乎所有的Linux平台,都是默认自带这个库的,在Linux当中编写多线程代码,都需要使用第三方thread
库的。
注意:当使用gcc/g++
进行编译时,需要带上-lpthread
选项,来引入线程库
如g++ -o $@ $^ -std=c++11 -lpthread
线程创建
了解完线程库之后,我们需要先知道一个概念,叫做TID
,是线程的唯一标志符。对线程的所有操作,都是基于这个TID
的。注意:所有的线程操作都需要包含头文件<pthread.h>
。
pthread_create
功能:创建一个新的线程
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数:
thread
:是一个输出型参数,用于接收新创建线程的标识符。attr
:用来指定线程的属性,如线程的栈大小、调度策略等。一般用不到这个参数,设置为nullptr
即可。start_routine
:类型是void *(*)(void*)
的函数指针,是新线程开始执行的函数。arg
:传给线程启动函数的参数,也就是给start_routine
函数传参。
返回值:成功返回0,失败返回错误码。
代码示例:
void *threadRun(void *args)
{while (1){cout << "new thread:" << getpid() << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, nullptr);while (1){cout << "main thread:" << getpid() << endl;sleep(1);}return 0;
}
先创建一个类型为pthread_t
的tid,然后通过函数接收tid。
第三个参数是threadRun
,即用新创建线程去执行这个函数。现在就能看出有2个线程在同时运行,它们的pid都是一样的,说明是有在进程中运行的。
我们通过ps -ajx
来观察一下进程的状态。
可以看到,只有一个进程,所以说明这2先线程属于同一个进程。要想观察到线程的状态,可以使用ps -aL
指令。
从结果中可以看出,确实有2个线程,当PID和LWP相同说明这个线程是主线程,其它的线程都是通过这个主线程创建出来的。
pthread_self
功能:用于得到当前线程的TID
函数原型:pthread_t pthread_self(void);
没有参数,所以这个函数一定会成功返回,返回一个线程ID。
int main()
{pthread_t n = pthread_self();cout << n << endl; // 139726987786048return 0;
}
线程终止
pthread_exit
功能:退出当前线程。
函数原型:void pthread_exit(void *retval);
参数:retval
是一个指针,可以传递一个值给等待这个线程退出的其他线程(通过 pthread_join
获取)。
在使用这个函数之前,我们先在其中一个线程中使用exit
看看会有什么结果。
void *threadRun(void *args)
{string name = (char *)args;int count = 5;while (count--){cout << "new thread: " << name << endl;sleep(1);}exit(0);return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, (void *)"hello thread");while (1){cout << "main thread: " << getpid() << endl;sleep(1);}return 0;
}
在上面的代码中,我们在创建出来的线程中添加了exit(0)
代码,主线程进行死循环,新线程循环5次后执行exit(0)
代码。通过代码我们可以想到,当新线程退出后,应该会一直执行主线程中的内容,但是结果是整个代码直接退出。
由此可得,exit接口的作用是终止整个进程。只要其中一个线程执行exit接口,就会造成整个进程终止。所以我们不能使用exit来退出线程。
需要注意,pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel
功能:取消一个执行中的线程
函数原型:int pthread_cancel(pthread_t thread);
参数thread
是要取消线程的TID。
总结:如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_join
功能:等待线程结束。
线程等待的目的有两个:
- 获取新线程的退出信息(可以不关心,但是必须要等)
- 回收新线程对应的PCB等内核资源,防止内存泄漏(暂时无法查看)
函数原型:int pthread_join(pthread_t thread, void **retval);
参数:
thread
:等待的线程ID。retval
:输出型参数,线程退出后,该参数会接收到线程的函数返回值
返回值:成功返回0,失败返回错误码。
void *threadRun(void *args)
{string name = (char *)args;int count = 5;while (count--){cout << "new thread :" << name << endl;sleep(1);}return (void *)100; // 走到这里默认线程退出
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");void *ret;pthread_join(tid, &ret);cout << "main thread :" << (long long)ret << endl;return 0;
}
可以从结果中看出,当创建的线程循环5次后,返回值是100。主线程中通过pthread_join
等待主线程结束,并且把返回值保存到ret
当中。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,
retval
所指向的单元里存放的是thread线程函数的返回值。 - 如果thread线程被别的线程调用
pthread_ cancel
异常终掉,retval
所指向的单元里存放的是常数PTHREAD_ CANCELED
- 如果thread线程是自己调用pthread_exit终止的,
retval
所指向的单元存放的是传给pthread_exit
的参数。 - 如果对thread线程的终止状态不感兴趣,可以传NULL给
retval
参数。
主线程等待的时候,默认是阻塞等待的。
线程ID及地址空间布局
string toHex(pthread_t tid) // 转换成16进制
{char hex[64];snprintf(hex, sizeof(hex), "%p", tid);return hex;
}void *threadRun(void *args)
{while (true){cout << "thread id: " << toHex(pthread_self()) << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_t n1 = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");cout << "main thread create thead done, new thread id : " << toHex(tid) << endl;pthread_join(tid, nullptr);return 0;
}
从结果可以看出,有点像地址,所以tid就是线程控制块在共享区中的起始地址。根据这个地址就可以找到对应线程的属性了。
线程的控制是通过线程库实现的,而pthread
库是一个动态库。那么该如何管理线程呢?当然是先用结构体描述出线程,同时再被数据结构所组织起来,这样才能管理好一群线程,这个任务就是由线程库来实现的。
每个线程都有独立的栈结构,但是虚拟地址的栈结构只有一个,那么线程的栈结构在哪呢?
答案就是线程的栈在库中对应的TCB当中。也就是说,当创建多个线程的时候,主线程的栈在进程地址空间当中,而其它线程的独立栈,都在共享区当中,具体来讲是在pthread
库当中。
库帮我们创建轻量级进程,调用的接口是clone,当TCB创建好后,把栈空间的起始地址传递给child_stack,而未来每个新线程都用的是自己独立栈。不和主线程用同一个栈。
每个线程的栈位于进程地址空间中的栈区域。在 Linux 中,进程地址空间一般从高地址向低地址增长,栈通常位于较高的虚拟地址区域。不同线程的栈在这个区域内有各自独立的内存范围。
在之前我们讲过,所有的线程都是共享同一个进程地址空间的。这也就是说多个线程可以同时访问同一个变量,只要其中一个线程修改了这个全局变量,那么其它线程访问这个变量的时候就会收到影响。
int g_val = 5;void *threadRun(void *args)
{cout << "g_val = " << g_val << " &g_val = " << &g_val << endl;return nullptr;
}int main()
{pthread_t tid;pthread_t n1 = pthread_create(&tid, nullptr, threadRun, nullptr);sleep(1);cout << "g_val = " << g_val << " &g_val = " << &g_val << endl;return 0;
}
我们可以看到主线程和新创建的线程访问的都是同一块变量。如果我们在改一下代码看看又是什么结果。
void *threadRun(void *args)
{g_val += 2;cout << "new thread:" << "g_val = " << g_val << " &g_val = " << &g_val << endl;return nullptr;
}
我们在新线程中对全局变量加2,然后主线程的全局变量也被改变了。这样就证明了开始说的所有线程都是共享同一份资源的。
但是如果我们想要对于同一个变量,每个线程都各自维护一份,互不影响,能否做到呢?答案是肯定可以的,此时就需要用到线程局部存储。只需要在变量前面加上__thread
即可(2个下划线)
__thread int g_val = 5;
,接着我们再次运行刚才的代码。
这次两个线程的g_val
的地址不一样了。
需要注意的是局部存储变量只能对内置类型起作用,对于自定义类型会报错。
可能有人注意到一点,就是从全局变量到局部存储的过程中,g_val
的地址变得很大了,把new thread
的地址转换成10进制我们可以发现变量地址由93,898,956,742,672
到139,947,049,457,212
。我们在把main thread
的地址转换成10进制:139,947,049,461,564
。
为什么会变得这么大呢?
原因是刚开始定义的全局变量在已初始化数据段,而设置成线程局部存储,就到了对应线程的TCB当中,线程局部存储是在共享区当中,这段空间已经接近栈了,比已经初始化的数据段要高。而主线程的地址又比共享区中的线程的地址大,我们要知道虚拟地址的空间是从低地址到高地址增长的,所以地址大小是:已初始化数据段<共享区地址<主线程栈
线程与进程的关系
线程和进程是两个密切相关的概念,一个进程至少拥有一个线程(该线程为主线程),进程根据需要可以创建若干个线程。在多线程环境中,进程仍然有一个进程控制块和用户地址空间,但是每个线程都有自己独立的堆栈和线程控制块,在线程控制块中包含该线程执行时寄存器的值、线程的优先级及其它与线程相关的状态信息,所以说,线程基本上不用有资源,只拥有少量必不可少的资源(线程控制块和栈)
线程共享进程数据,但也不是所有资源都共享。线程也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
当线程切换时,CPU 会保存当前线程的寄存器状态,并恢复要切换到的线程的寄存器状态,以确保每个线程都能独立地执行,互不干扰。
- 栈:
每个线程都有自己独立的栈。主要用于存储函数调用的局部变量、函数参数、返回地址等。线程在执行函数调用时,会在栈上分配空间来存储这些信息。
- errno
线程之间的错误码各自独立。
- 信号屏蔽字
信号屏蔽字(signal mask)也称为信号集,用于指定一个线程当前要屏蔽哪些信号。当一个信号被屏蔽时,该信号会被暂时搁置,不会立即递送给线程。每个线程都有自己独立的信号屏蔽字。
- 调度优先级
在多线程环境中,可以为不同的线程设置不同的调度优先级,以满足特定的任务需求。例如,对于实时性要求较高的任务,可以设置较高的优先级,确保其及时执行。调度优先级通常是一个整数,数值越大表示优先级越高。
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id。
进程中的所有线程共享该进程的资源,它们驻留在同一块地址空间当中,并且可以访问相同的数据。当一个线程改变了内存中某个单元的数据时,其他线程在访问该数据单元时会看到变化后的结果,线程之间的通信变得更为简单、容易。
线程的优缺点
优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
线程的异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
void *threadRun(void *args)
{int cnt = 5;while (cnt--){cout << "new thread, cnt: " << cnt << endl;sleep(1);}int a = 3;a /= 0;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, nullptr);while (1){cout << "main thread" << endl;sleep(1);}pthread_join(tid, nullptr);return 0;
}
这段代码是让主进程一直输出main thread
,而让新创建的线程输出5次new thread
之后,进行除0。这时就发生了错误,导致整个进程都退出了。因为任何一个线程被干掉了,默认整个进程都会被干掉,这个信号是发送给进程 的,因为每一个线程发送的信号都是进程的执行分支。
相关文章:
线程的概念和控制
自从20世纪60年代提出了进程的概念之后,操作系统一直以进程作为独立运行的基本单位。到了20世纪80年代,人们又提出了比进程更小的、能独立运行的基本单位——线程。提出线程的目的是试图提高系统并发执行的程度,从而进一步提高系统的吞吐量。…...
如何配置activemq,支持使用wss协议连接。
1、到阿里云申请一个证书,通过后下载jks证书。 2、配置activemq: 打开activemq安装目录中“conf/activemq.xml”,增加以下记录: <transportConnectors> <transportConnector name"wss" uri"…...
【言语】刷题3
front:刷题2 题干 超限效应介绍冰桶挑战要避免超限效应 B明星的作用只是病痛挑战的一个因素,把握程度才是重点,不是强化弱化明星作用,排除 A虽没有超限效应,但是唯一的点出“冰桶效应”的选项,“作秀之嫌…...
关于 ast: Babel AST 全类型总览
AST 的每个节点都有一个 type 字段,用来标识它的语法类型。 程序结构节点 type说明示例Program整个程序的根节点整体代码结构BlockStatement大括号代码块 {}if、function、for 等的主体ExpressionStatement表达式语句(如 a b;)EmptyStatem…...
STM32 内存
根据STM32的存储器映射机制,其32位地址总线可访问4GB逻辑地址空间(0x00000000-0xFFFFFFFF),但实际物理地址分配由芯片厂商定义。以下是STM32完整的地址映射结构及关键区域说明: 一、地址空间整体架构 4GB地址空间划分…...
图片的require问题
问题 <template><!--第一种方式--><img :src"require(/assets/${imageName})" style"width:100px;" /><!--第二种方式--><img :src"require(imageUrl)" style"width:100px;" /> </template><…...
关于 js:8. 反调试与混淆识别
一、常见反调试手段识别 1. debugger 死循环(阻塞调试器) 样例代码: while (true) {debugger; }原理: 每次执行到 debugger 语句,如果 DevTools 打开,将自动触发断点。 如果在死循环中,调试…...
深度Q网络(DQN)的基本概念
一、深度Q网络(DQN)的基本概念 深度Q网络(Deep Q-Network,DQN)是将强化学习中的Q学习(Q-Learning)与深度学习相结合的算法,由DeepMind在2013年提出,并在2015年发表于《Nature》杂志。它通过神经网络近似动作价值函数(Q函数),解决传统Q学习在高维状态空间下的计算难…...
uniapp+vue3中自动导入ref等依赖
前言: 在我们使用uni-appvue3创建项目,开发的过程中,老是需要导入我们的ref、onshow等,那么能不能自动导入,不用我们每个页面都写呢?是没问题的,这里让他的小帮手来帮你减轻负担:他就…...
合肥SMT贴片加工核心优势与工艺升级
内容概要 在电子制造领域,工艺精度与生产效率的平衡始终是企业关注的核心命题。本文将系统呈现合肥SMT贴片加工产业的技术演进图谱,为寻求制造升级的企业提供可落地的决策参考。 作为长三角电子制造集群的重要节点,合肥SMT贴片加工产业通过持…...
Ansible安装与核心模块实战指南
Ansible安装与核心模块实战指南 自动化运维入门:从安装到模块化任务配置 Ansible作为一款无代理自动化工具,通过模块化设计实现高效管理,尤其适用于快速部署、配置和维护大规模系统。本文将从安装、核心模块使用到实际案例,全面解析其核心功能与最佳实践。 一、Ansible安装…...
TDengine 做为 Spark 数据源
简介 Apache Spark 是开源大数据处理引擎,它基于内存计算,可用于批、流处理、机器学习、图计算等多种场景,支持 MapReduce 计算模型及丰富计算操作符、函数等,在大超大规模数据上具有强大的分布式处理计算能力。 通过 TDengine …...
Codeforces Round 997 (Div. 2)
A. Shape Perimeter 题目大意 给你一个m*m的正方形,再给你n个坐标表示每次在xy移动的距离(第一个坐标是初始位置正方形左下角),问路径图形的周长 解题思路 记录好第一次的位置之后一直累加最后求总移动距离的差值即可 代码实…...
WSL 安装 Debian 12 后,Linux 如何安装 nginx ?
在 WSL 的 Debian 12 中安装 Nginx 的步骤如下: 1. 更新系统软件包 sudo apt update && sudo apt upgrade -y2. 安装 Nginx sudo apt install nginx -y3. 管理 Nginx 服务 ▶ 启动 Nginx sudo service nginx start # 如果使用 systemd 可能需改用&…...
目标检测任务 - 数据增强
目标检测任务 - DETR : 数据预处理/数据增强 算法源码实例 import datasets.transforms as Tnormalize T.Compose([T.ToTensor(),T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])scales [480, 512, 544, 576, 608, 640, 672, 704, 736, 768, 800]…...
java的switch case
import java.util.Scanner;public class Hello {public static void main(String[] args) {Scanner in new Scanner(System.in);int type in.nextInt();switch(type){case 1:case 2:System.out.println("你好");break;case 3:System.out.println("晚上好"…...
基于亚博K210开发板——LCD触摸屏读取坐标数据测试
开发板 亚博K210开发板 实验目的 主要学习 K210 通过 I2C 读取触摸屏的坐标,并打印出来,显示在 LCD上。 实验准备 实验元件 LCD 显示屏触摸板 元件特性 K210 开发板自带 2.0 寸触摸屏,其实是 LCD 显示屏上贴一个触摸板组成…...
coze平台实现文生视频和图生视频(阿里云版)工作流
工作流全貌 开始 首先从入参开始: api_key:来自阿里云百炼平台,自行去申请 prompt:生成视频的文本提示词。支持中英文,长度不超过800个字符,每个汉字/字母占一个字符,超过部分会自动截断。 …...
python酒店健身俱乐部管理系统
目录 技术栈介绍具体实现截图系统设计研究方法:设计步骤设计流程核心代码部分展示研究方法详细视频演示试验方案论文大纲源码获取/详细视频演示 技术栈介绍 Django-SpringBoot-php-Node.js-flask 本课题的研究方法和研究步骤基本合理,难度适中…...
QtGUI模块功能详细说明,图标和光标(七)
目录 一.窗口和屏幕管理 二. 绘图和渲染 三. 图像处理 四. 字体和文本 五. 事件和输入处理 六. OpenGL 和硬件加速 七. 颜色和外观 八. 图标和光标 1、QIcon: 图标管理 1.1、QIcon 简介 1.2、图标的来源与创建 1.3、多分辨率与 DPI 支持 1.4、图标的状态管理 2、…...
【图像处理基石】如何入门OCR技术?
入门OCR(Optical Character Recognition,光学字符识别)技术需要结合理论学习、工具实践和项目实战,以下是分步骤的学习指南,适合零基础学习者: 一、明确OCR技术的核心概念 OCR的基本原理 核心流程…...
数据库知识沉浸式游戏化学习设计研究
数据库知识沉浸式游戏化学习设计研究 摘要: 本研究旨在设计一款以数据库知识为主题的沉浸式游戏化学习系统。通过对数据库知识体系的深入剖析,结合游戏化学习理论,构建了一个多层次、多任务的游戏架构。玩家在游戏过程中需完成构建数据库结构、编写 SQL 查询等任务来解锁关…...
大疆无人机
在大疆上云API中,DRC 链路通常指 Device-Cloud Remote Control Link(设备-云端远程控制链路),它是无人机(或设备)与云端服务之间建立的实时控制与数据传输通道,用于实现…...
撤回不了一点 v1.0.2,支持微信QQ钉钉飞书等消息防撤回
如今生活节奏快得飞起,社交软件和工作通讯软件成了咱日常交流的核心阵地。大家肯定都有过这些闹心事儿:和朋友聊得正嗨,对方突然撤回一条消息,好奇心瞬间爆棚,却怎么也看不到撤回的内容;工作群里关键信息刚…...
什么是Git?
“Git”是目前非常火、广泛使用的版本控制系统,尤其在软件开发领域中扮演着核心角色。 一、什么是Git?它到底是什么? Git 是一种版本控制系统(Version Control System, VCS)。它的主要作用是帮助开发者管理“代码的不…...
微信小程序 自定义图片分享-绘制数据图片以及信息文字
一 、需求 从数据库中读取头像,姓名电话等信息,当分享给女朋友时,每个信息不一样 二、实现方案 1、先将数据库中需要的头像姓名信息读取出来加载到data 数据项中 data:{firstName:, // 姓名img:, // 头像shareImage:,// 存储临时图片 } 2…...
langchain提示词的使用
一、概述 提示词是指向人工智能大模型提供的输入信息,通常包含关键词、问题或指令,可以引导大模型生成与用户期望相符的回应。我们在豆包,DeepSeek等大模型中输入的问题都可以认为一个简单的提示词,不过为了真正得到我们需要的结…...
C语言| extern的用法作用
C语言| 局部变量、全局变量 extern定义的变量,只对全局变量有用。 掌握extern的用法及其作用。extern主要用于在不同.c文件间扩展全局变量的作用范围。 扩展全局变量的使用范围,操作方法: 1 在一个文件内扩展全局变量的使用范围 全局变量…...
Rust 环境变量管理秘籍:从菜鸟到老鸟都爱的 dotenv 教程
前言 写代码的你,是否遭遇过这些灵魂拷问: “我现在在哪个环境?开发?测试?还是直接在生产线上裸奔?”“少写一个 .env,测试脚本在数据库里上演清空大法,客户当场破防。”“每次手动设置 RUST_ENV,命令敲到一半就开始怀疑人生,还怕输错一个字符引发灭世级事故。”别慌…...
Leetcode (力扣)做题记录 hot100(49,136,169,20)
力扣第49题:字母异位词分组 49. 字母异位词分组 - 力扣(LeetCode) 遍历数组,将每一个字符串变成char数组 然后排序,如果map里面有则将他的值返回来(key是排序好的字符串) class Solution {pu…...
Slitaz 系统深度解析
Slitaz 系统深度解析:从系统架构到设计哲学 一、系统定位与核心目标 Slitaz(Simplified Lightweight IT Automatic Zen)是一个基于 Linux 的超轻量级发行版,设计目标是极致轻量化、快速启动、低资源消耗,专为老旧硬件…...
Deepseek+Xmind:秒速生成思维导图与流程图
deepseekxmind,快速生成思维导图和流程图 文章目录 思维导图deepseek笔记本 txt文件xmind 流程图deepseekdraw.io 思维导图 deepseek 笔记本 txt文件 将deep seek的东西复制到文本文件中,然后将txt文件拓展名改成md xmind 新建思维导图----左上角三…...
理解计算机系统_并发编程(5)_基于线程的并发(二):线程api和基于线程的并发服务器
前言 以<深入理解计算机系统>(以下称“本书”)内容为基础,对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定 引入 接续上一篇理解计算机系统_并发编程(4)_基于线程的并发(一…...
java刷题基础知识
List<int[]> merged new ArrayList<int[]>(); return merged.toArray(new int[merged.size()][]); 表示一个存储 int[] 类型元素的列表,list灵活支持扩展,因为不知道最后有几个区间,所以用list,最后toArray返回成数组…...
MATLAB语音情感识别神经网络方法
在MATLAB中使用神经网络进行语音情感识别通常涉及以下步骤:数据准备、特征提取、神经网络模型构建、训练与评估。以下是详细说明和示例代码: 1. 数据准备 数据集:推荐使用公开情感语音数据集(如RAVDESS、CREMA-D、EMODB等&#x…...
PostgreSQL 服务器信号函数
PostgreSQL 服务器信号函数 PostgreSQL 提供了一组服务器信号函数(Server Signaling Functions),允许数据库管理员向 PostgreSQL 服务器进程发送特定信号以控制服务器行为。这些函数提供了对数据库服务器的精细控制能力。 一、核心信号函数…...
流动式起重机Q2的培训内容有哪些?
流动式起重机 Q2 的培训内容主要分为理论知识和实际操作两部分,具体如下: 理论知识 基础理论知识:涵盖机械原理、液压原理、电气原理等内容,帮助学员理解起重机的基本工作原理。例如,通过机械原理知识,学员…...
虹科应用 | 探索PCAN卡与医疗机器人的革命性结合
随着医疗技术的不断进步,医疗机器人在提高手术精度、减少感染风险以及提升患者护理质量方面发挥着越来越重要的作用。医疗机器人的精确操作依赖于稳定且高效的数据通信系统,虹科提供的PCAN四通道mini PCIe转CAN FD卡,正是为了满足这一需求而设…...
Linux系统编程---Signal信号集
0、前言 在上一篇博客笔记文章中,对Linux进程间通信的信号进行了讲解,本章将接着上一篇文章的内容,继续对Linux进程间通信中信号部分的信号集这个小知识点进行梳理。 如果有对Linux系统编程有不了解的地方,欢迎查阅博主的Linux系统…...
上电单次复位触发电路
SA1相当于是另外一个触发信号,S2A是手动触发信号,当S1A和S2A开关都断开时,示波器A入口所连接线路为上拉状态,高电平为3V。 当S2A闭合,相当于手动拉低,可以用于唤醒单片机之类的。 当S1A闭合,模拟电源接入&…...
talk-linux 不同用户之间终端通信
好的!下面是一个完整的指南和脚本,用于在两台 Linux 主机上配置并使用 talk 聊天功能(假设它们在同一个局域网内)。 ⸻ 🧾 一、需求说明 我们需要在两台主机上: 1. 安装 talk 和 talkd 2. 启用 talkd 服…...
QGIS 将 Shapefile 导入 PostGIS 数据库
一、背景介绍:QGIS、PostgreSQL 和 PostGIS 的关系和用途 在开始动手操作之前,我们先简单了解一下 QGIS、PostgreSQL 和 PostGIS 之间的关系及其用途。 QGIS(Quantum GIS):一款开源免费的桌面地理信息系统࿰…...
《内网渗透测试:绕过最新防火墙策略》
内网渗透测试是检验企业网络安全防御体系有效性的核心手段,而现代防火墙策略的持续演进(如零信任架构、AI流量分析、深度包检测)对攻击者提出了更高挑战。本文系统解析2024年新型防火墙的防护机制,聚焦协议隐蔽隧道、上下文感知绕…...
CSS结构性伪类、UI伪类与动态伪类全解析:从文档结构到交互状态的精准选择
一、结构性伪类选择器:文档树中的位置导航器 结构性伪类选择器是CSS中基于元素在HTML文档树中的层级关系、位置索引或结构特征进行匹配的一类选择器。它们无需依赖具体的类名或ID,仅通过文档结构即可精准定位元素,是实现响应式布局和复杂文档…...
【大模型LLM学习】MiniCPM的注意力机制学习
【大模型LLM学习】MiniCPM的注意力机制学习 前言1 Preliminary1.1 MHA1.2 KV-cache 2 GQAGQA的MiniCPM实现 3 MLAMLA的MiniCPM-3-4b的实现 TODO 前言 之前MiniCPM3-4B是最早达到gpt-3.5能力的端侧小模型,其注意力机制使用了MLA。本来想借着MiniCPM从MHA过到MLA的&am…...
stm32之PWR、WDG
目录 1.PWR1.1 简介1.2 电源框图1.3 上电复位和掉电复位1.4 可编程电压监测器1.5 低功耗模式1.5.1 模式选择1.5.2 睡眠模式1.5.3 停止模式1.5.4 待机模式 1.6 实验1.6.1 修改主频1.6.2 睡眠模式串口发送接收1.6.3 停止模式对射式红外传感器计次1.6.4 待机模式实时时钟 2.看门狗…...
分布式任务调度XXL-Job
XXL-Job 是一款轻量级、分布式的任务调度平台,其核心设计解决了传统任务调度(如Quartz)在分布式场景下的任务分片、高可用、可视化管控等痛点。以下从原理、核心架构、应用场景、代码示例及关联中间件展开详解 一、主流任务…...
内存泄漏与OOM崩溃根治方案:JVM与原生内存池差异化排查手册
内存泄漏与OOM崩溃根治方案:JVM与原生内存池差异化排查手册 一、问题描述与快速解决方案 1. 核心问题分类 内存泄漏(Memory Leak) 现象:应用运行时间越长,内存占用持续攀升,GC回收效率下降,最…...
火山引擎发展初始
火山引擎是字节跳动旗下的云计算服务品牌,其云服务业务的启动和正式商业化时间线如下: 1. **初期探索(2020年之前)** 字节跳动在早期为支持自身业务(如抖音、今日头条等)构建了强大的基础设施和技术中…...
使用光标测量,使用 TDR 测量 pH 和 fF
时域反射计 (TDR) 是一种通常用于测量印刷电路板 (PCB) 测试试样和电缆阻抗的仪器。TDR 对于测量过孔和元件焊盘的电感和电容、探针尖端电容和电感,甚至寄生电感收发器耦合电容器也非常有用。这也是验证仿真或提取您自…...