当前位置: 首页 > news >正文

【Linux】线程

一.线程概念

我们在学习进程的时候已经知道了,进程=内核数据结构pcb+自己的代码和数据那么单单一个task_struct是什么呢?

我们将单个的task_struct叫做轻量级进程,而这个轻量级进程也叫做线程。以往我们在了解进程的时候,一个进程内部都只有一个task_struct,也就是一个进程内部只有一个线程。而这其实是一种特殊情况。更一般的情况下,一个进程内部有多个线程共享同一个虚拟地址空间。

我们在访问系统资源的时候,大部分都是通过虚拟地址空间来访问的。也就是说虚拟地址空间是资源的窗口,进程内部的所有的线程都可以看到该窗口,即都可以访问该进程的资源。每一个线程有自己的入口函数,对于主进程来说,它的入口就是main函数。而线程执行不同的代码和数据,本质就是在进程内部对资源的划分,其实就是对地址空间的划分,每个线程都执行分给自己的那部分虚拟地址。

在创建进程的时候,我们要创建内核数据task_struct,创建虚拟地址空间,加载物理内存等等,而这些都是系统资源。所以,进程是承担分配的系统资源的基本实体!线程是进程内部的一个执行流分支,是cpu调度的基本单位。

也就是说,站在cpu的角度,它不用区分当前占用cpu资源的到底是进程还是线程了,cpu只关心一个一个的执行流。无论一个进程内部有多个执行流,cpu都是以task_struct为基本单位进行调度的,也就是线程。

Linux操作系统实现线程的方式是利用已有的进程的代码,进行复用,用进程来模拟线程。Linux操作系统中没有线程的概念,只有轻量级进程的概念,它是利用系统调用在用户层实现了用户级线程。

而在windows下,专门设计出了tcb结构用来描述线程。一个进程pcb中,还包含了一个tcb的链表结构,保存了该进程内部的所有的线程。windows这样的设计无异于让进程和线程变得更加复杂了。

二.分页式存储管理 

1.分页式管理

学到这里,我已经知道,我们对物理内存的管理实际上是通过虚拟地址空间再加上页表的映射来实现的。

我们的二进制文件在磁盘上是以4kb为单位的数据块存储的,而运行可执行程序,其向内存中加载到时候也是以4kb为单位的,并且物理内存也被分成了以4kb为基本单位的内存块。

我们将这个4kb的基本内存块或者数据块叫做页框或者页帧。

假设我们的物理内存有4GB,一个页框的大小是4kb,那么整个物理内存就被分为了1048576个页框。

这么多页框,操作系统要对其进行管理,所以先描述,再组织。在Linux操作系统中,就是通过page结构体来描述一个一个的页框的。

struct page
{/* 原⼦标志,有些情况下会异步更新 */unsigned long flags;union{struct{/* 换出⻚列表,例如由zone->lru_lock保护的active_list */struct list_head lru;/* 如果最低为为0,则指向inode* address_space,或为NULL* 如果⻚映射为匿名内存,最低为置位* ⽽且该指针指向anon_vma对象*/struct address_space *mapping;/* 在映射内的偏移量 */pgoff_t index;/** 由映射私有,不透明数据* 如果设置了PagePrivate,通常⽤于buffer_heads* 如果设置了PageSwapCache,则⽤于swp_entry_t* 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶*/unsigned long private;};struct{ /* slab, slob and slub */union{struct list_head slab_list; /* uses lru */struct{ /* Partial pages */struct page *next;
#ifdef CONFIG_64BITint pages;    /* Nr of pages left */int pobjects; /* Approximate count */
#elseshort int pages;short int pobjects;
#endif};};struct kmem_cache *slab_cache; /* not slob *//* Double-word boundary */void *freelist; /* first free object */union{void *s_mem;            /* slab: first object */unsigned long counters; /* SLUB */struct{                        /* SLUB */unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */unsigned objects : 15;unsigned frozen : 1;};};};...};union{/* 内存管理⼦系统中映射的⻚表项计数,⽤于表⽰⻚是否已经映射,还⽤于限制逆向映射搜索*/atomic_t _mapcount;unsigned int page_type;unsigned int active; /* SLAB */int units;           /* SLOB */};...
#if defined(WANT_PAGE_VIRTUAL)/* 内核虚拟地址(如果没有映射则为NULL,即⾼端内存) */void *virtual;
#endif /* WANT_PAGE_VIRTUAL */...
}

page中有一个flags标志位,它表明了该页的状态,是被使用了,还是没有被使用,还是只读的等等! 

那操作系统是怎么将这么多的页管理起来的呢?通过一个全局的数组。struct page mem[1048576];这样,每一个page都会有一个下标,我们里利用下标*4kb,就可以拿到指定下标page的起始地址。

但是我们在使用虚拟地址时,都是访问某一个或者几个具体的字节,而不是一整个页框,所以,在page中,我们通过页框的起始物理地址+页内的偏移量,就可以访问到具体的某个字节。

 2.页表

我们访问物理内存都是通过访问虚拟地址+页表进行虚拟到物理内存的转化来实现的。那么页表的结构到底是什么呢?

Linux中的页表采取的是2级页表,分为外级页表和内级页表,外级页表叫做页目录,它保存的表项是内级页表,而内级页表叫做页表,它保存的表项为页框起始地址。

而在32位机器下,地址的大小是4字节,也就是32个比特位。当我们进行虚拟地址到物理地址的转换时:前10个比特位,范围[0,1023]共1024个,用来查找对应的页目录,中间10个比特位,用来查询页目录指定的页表,通过这一阶段,我们就找到了要访问的虚拟地址在物理内存中的页框。

但我们访问的是具体的字节,这就用到了剩下的12个比特位。这12个bit位一共有4kb个组合,正好对应一个页框的大小,它起始就是页内偏移量。这时虚拟转换为物理的第二阶段。

所以,我们是如何通过访问虚拟地址,间接访问物理内存的呢?

当我们访问虚拟地址的时候,cpu中的内存管理单元(MMU)会先拿着虚拟地址的前10位作为下标,查看页目录而页目录会保存在cpu的CR3寄存器中,找到对应页表的起始地址,再拿着中间10位作为下标查看页表,找到指定的页框,再通过最后12的页内偏移就可以找到页框内的具体字节。

单级页表会消耗大量内存空间,而多级页表会因为多次查询导致效率降低。而在计算机科学中,任何问题都可以通过加一个中间层来解决。

MMU引入了新武器,江湖人称快表的TLB(其实,就是缓存)。

当CPU给MMU传新虚拟地址之后,MMU先去问TLB那边有没有,如果有就直接拿到物理地址,但是TLB容量比较小,难免会遇到Cache Miss(缓存未命中),这时候MMU还有保底的老武器——页表,在页表中找到之后MMU除了拿到该物理地址进行使用外,还会把这条映射关系保存到TLB中。

三.线程的优缺点 

1.线程的优点

0x1.创建一个新线程的代价要比创建一个新进程小的多。

创建一个新进程只需要创建一个轻量级进程-task_struct就行了。而创建一个新进程不仅要创建内核数据结构,还得创建虚拟地址空间、加载物理内存等等操作。

0x2.线程占用的资源要比进程少的多

线程是进程中的一个执行单元,线程共享该进程内的所有资源,但也有自己独立的栈结构和上下文数据。

0x3.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

对于复杂的计算任务,如科学计算、机器学习算法中的矩阵乘法、物理模拟等,可将任务分解为多个子任务,每个子任务由一个线程处理,实现并行计算,减少任务的总体执行时间。

0x4.I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。

在 I/O 密集型应用中,程序经常需要等待 I/O 操作完成,如从磁盘读取文件、向网络发送数据等。如果使用单线程,在等待 I/O 操作完成期间,CPU 将处于空闲状态,资源利用率较低。而多线程可以充分利用 CPU 资源。当一个线程等待 I/O 操作时,其他线程可以继续执行,CPU 可以在不同线程之间切换,避免因等待 I/O 而导致的 CPU 空闲,从而提高 CPU 的利用率。

 0x5.线程的切换比进程的切换效率要高得多

当切换线程的时候,因为共享地址空间,所以只需要保存cpu的硬件上下文数据到线程的task_struct中即可。切换进程的时候,当然也需要保存cpu的硬件上下文数据,到pcb中的,但除了这些还需要切换页表,物理内存、虚拟地址空间等资源。

还有一些切换的隐藏的损耗就是缓存失效。当我们借助虚拟地址访问资源的时候,cpu中会有cache缓存将你要访问的资源那个页框都加载到缓存中。而当线程切换的时候,因为共享地址空间,所以cache缓存多少会有点用,但是对于进程切换来说,它的整个虚拟地址都变了,所以cache缓存就失效了,此时访问虚拟地址效率就比较低了,需要重新进行缓存。TLB快表页如此,进程切换之后,虚拟到物理的转换也变慢了。

2.线程的缺点 

0x1.性能损失

并不是线程越多越好,如果线程的数量比处理器的数量多,此时仍会有多个执行流闲置。并且频繁的切换线程也是会有消耗的

0x2.缺乏访问控制

多个线程可以同时访问和修改共享的变量或数据结构,如果缺乏适当的访问控制机制,就可能出现数据竞争问题。例如,两个线程同时对一个共享的计数器进行递增操作,可能会导致计数器的最终值比预期的要小,因为线程之间的操作相互干扰。所以,使用多线程可能会导致代码的健壮性降低。

当一个进程中的任意一个线程收到信号/或者异常了,整个进程就会结束。因为信号是发送给进程的。

3.进程和线程

进程是系统资源分配的基本实体,线程是进程的执行流分支,是cpu的基本调度单位。进程之间是相互独立的,资源互相不可见。而同一个进程中的线程,共享整个地址空间,但每个线程还有自己独立的数据/代码:

  • 线程ID
  • 一组寄存器:cpu调度的基本单位是线程,线程就是轻量级进程task_struct。当cpu进行切换的时候,需要把线程的上下文保存在自己的数据结构中。以便下一次继续执行。
  • 线程栈
  • errno
  • 信号屏蔽字
  • 调度优先级

四.线程控制 

demo

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>// 新线程入口
void* routine(void* arg)
{std::string name = static_cast<char*>(arg);std::cout << "i am " << name << std::endl;sleep(10);return nullptr;
}int main()
{// 创建线程pthread_t tid;pthread_create(&tid, nullptr, routine, (void*)"thread-1");std::cout << "i am main thread" << std::endl; // 回收线程pthread_join(tid, nullptr);return 0;
}

说明:因为Linux是用轻量级进程模拟实现的线程,所以在Linux操作系统中并没有创建现成的系统调用。只有创建轻量级进程的系统调用。所以我们上面使用的pthread库其实是对Linux操作系统轻量级进程的封装,实现了用户级的线程。

我们可以使用ps -aL来查看当前的所有线程:

 因为pthread库是第三库,所以我们在编译的时候需要加上 -l pthread选项。

我们看图发现,这两个线程的pid是一致的,它们是属于同一个进程的。第二个LWP的意思是light weight process 轻量级进程,我们看到第一个相乘的lwp与进程pid相同,说明它是主线程。 

下面是Linux操作系统提供的创建轻量级进程的系统调用。 

NAMEvfork - create a child process and block parentSYNOPSIS#include <sys/types.h>#include <unistd.h>pid_t vfork(void);NAMEclone, __clone2, clone3 - create a child processSYNOPSIS/* Prototype for the glibc wrapper function */#define _GNU_SOURCE#include <sched.h>int clone(int (*fn)(void *), void *stack, int flags, void *arg, .../* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

c++11还提供了多线程,这个库是语言提供的。所以它得适配所有的平台。在Windows下,因为本来就又线程这个具体的概念,所以在Windows下就是通过封装线程的接口实现的。而在Linux操作系统下,因为没有具体的线程概念,线程是通过轻量级进程实现的,所以只有pthread库来实现线程操作,那么对于C++11而言,要想提供语言级的线程库,就只能对pthread库进行封装了。

1.线程控制接口

0x1.pthread_create()

NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

创建一个新线程

  • pthread_t * thread:线程id,输出型参数
  • *attr:创建线程的具体属性,我们不管,直接传nullptr即可
  • start_routine:函数指针,参数和返回值都是void*,作为新线程的入口函数
  • arg:新线程入口函数的参数

其实pthread_t其实就是一个无符号长整数。 

如上图,我们创建线程之后,主线程继续向下执行,而新线程的入口函数为routine,就跳转到该函数进行执行,执行完后,该线程也就结束了。但是观察打印的结果,我们有三个问题:

1.线程id为什么这么大,为什么不用lwp

lwp是轻量级进程的概念,如果直接将lwp作为线程id,这让没有了解过Linux操作系统的人会产生误解。而线程id到底是什么,后面再说。

2.主线程和新线程消息混在一起

因为主新线程共享同一个地址空间,也就共享同一个文件描述符表,它们都可以向显示器文件打印信息,但这也说明它们不是原子的,导致了数据的不一致。

3.收到信号, 进程直接退出。

信号是发送给进程的,进程收到信号会结束,线程自然也就结束了。当进程中,任意一个线程异常了,整个进程也会退出。

4.线程时间片

时间片是用来切换线程的计数器。在进程切换时,我们说当一个进程的时间片超时了,就切换另一个进程。而当一个进程内部有多个线程时,每个线程平分进程的时间片 

0x2.pthread_join

当线程从入口函数进入,执行完该函数后,就表明该线程结束了,与子进程结束父进程要等待一样,新线程结束了,主线程要对新线程进行等待,回收新线程的资源。如果不回收,就会产生类似僵尸进程的结果,也就是内存泄露问题。

NAMEpthread_join - join with a terminated threadSYNOPSIS#include <pthread.h>int pthread_join(pthread_t thread, void **retval);
  • thread:回收的线程id
  • retval:用于接收线程的退出信息。线程结束返回void*,可以利用该函数的输出型参数拿到

线程结束我们无法直接观察到类似僵尸进程的结果,但是我们要记住要等待新线程,避免内存泄露。

0x3.线程终止 

当我们的创建的新线程执行到入口函数的return语句就表示该线程终止了。当然,还有一些接口可以让我们手动终止线程:

NAMEpthread_exit - terminate calling threadSYNOPSIS#include <pthread.h>void pthread_exit(void *retval);NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include <pthread.h>int pthread_cancel(pthread_t thread);

pthread_exit让一个线程终止,其会返回void*参数到主线程

pthread_cancel取消一个线程,一般是主线程去取消其他新进程,取消之前需要保证该线程已经启动。

 0x4.线程分离

在父子进程,子进程结束父进程要等待子进程,子进程结束会向父进程发送SIGCHLD信号,如果父进程不关心子进程的退出信息,就可以采取忽略捕捉该信号,子进程就不会进入僵尸状态,直接退出。

在线程这里,主线程等待新线程时只能阻塞式的等待,无法向进程那样非阻塞等待,当我们不关心其他线程的返回结果时,阻塞等待非常浪费效率。所以我们可以让线程进行分离,分离之后,主新线程还是共享地址空间,只不过此时主线程就不必等待新进程了,新进程退出后,会直接释放其内核数据结构。

NAMEpthread_detach - detach a threadSYNOPSIS#include <pthread.h>int pthread_detach(pthread_t thread);NAMEpthread_self - obtain ID of the calling threadSYNOPSIS#include <pthread.h>pthread_t pthread_self(void);

线程分离需要提供线程id,所以我们自然可以让主线程分离新线程。但pthread库还提供了pthread_slef接口,用来获取调用该接口的线程的线程id,这样我们也可以实现新线程主动分离主线程。

需要注意的是,一个进程不能既是joinable又是分离的,也就是说当一个线程分离了,我们就不能再join了,这样的行为是未定义的。

 五.线程id及进程地址空间分布

Linux操作系统没有实际上的线程,是借助轻量级线程模拟的,所以就有了pthread原生线程库。既然是动态库,那么就是ELF格式的,当我们使用该库时,要将其加载到内存,同时将该库映射到该进程的虚拟地址空间上的共享区或者mmap动态区域。

因为动态库只会在内存中存在一份,所以所有的进程共享这一份pthread动态库。并且,线程这个概念是在库中定义的。所以,这个库中会包含许多的线程。那么我们就要对线程进行管理——先描述,再组织。

怎么理解在库中定义了线程呢?我们可以想想之前的FILE* fp = open;打开一个文件,其实就是为我们申请了一个FILE结构体对象,充当用户级缓冲区。所以对于在库中定义了线程,可以理解为我们使用pthread_create创建一个新线程,本质上就是为我们申请了一个strcut tcb结构体对象。

我们之间还说过,线程共享地址空间,但仍有自己独立的资源。比如独立的栈空间,独立的存储位置。

所以,为什么线程结束后需要join呢?因为库中的tcb结构,以及独立的资源需要被释放。

所以,线程id其实就是线程对应tcb控制块的虚拟地址。

所以,因为线程有自己独立的控制块和栈空间,线程在入口函数return 之后,会将内容保存在自己的存储空间中,然后我们主线程在join的时候又会从线程的存储空间中,拿出该退出信息。当我们创建新线程时也一样!!!

因为pthread库是对轻量级进程的接口的封装,所以我们在main函数中,pthread_create创建新线程的时候,除了创建线程控制块,还要调用底层封装的轻量级进程的系统调用,创建轻量级进程(task_struct)。

六.线程栈与线程局部存储

1.线程栈

除了主线程使用进程在地址空间上向下增长的栈空间外,其余的线程都是用自己独立的栈结构,虽然我们画图是将创建线程时的控制块和局部存储、线程栈画在了一起,但实际上,它们在库中并不连续,tcb中有指针指向线程自己独立的栈结构以及栈的大小。

主线程使用的栈空间是可以向下增长的,而新线程的栈空间是mmap出来的,大小是固定的,也可以在创建的时候指定大小。

虽然说是每一个线程独立的栈结构,但是所有的线程都共享该地址空间,且pthread库映射到了0~3GB的用户空间,其他线程是可以看到别的线程的栈上的数据的,但前提是得能拿到虚拟地址。

2.线程局部存储

线程局部存储(Thread - Local Storage,TLS)是一种为每个线程提供独立数据副本的技术,每个线程对自己的数据副本进行读写操作,不会影响其他线程的数据副本。比如说,我们有一个全局的变量,所有的进程共享该变量,都可以对该变量进行读写,且都是同时变化的。如果我们想让该变量不在因为其他线程修改了,所有的都要改变,就可以使用TLS。

使用场景:

当需要每个线程都有自己的特定数据,且这些数据不需要被其他线程访问或者每个线程对这些数据的操作相互独立时,TLS 就非常有用。例如,一些函数库可能需要为每个线程维护一些内部状态信息,像随机数生成器的种子、线程私有的缓冲区等,这时候就可以使用 TLS。

demo:

#include <iostream>
#include <pthread.h>
#include <unistd.h>int cnt = 0;// 线程1,修改并读cnt
void* routine1(void*)
{while(true){std::cout << "thread-1," << "cnt->" << cnt++ << std::endl;sleep(1);}return nullptr;
}// 线程2,只读cnt
void* routine2(void*)
{while(true){std::cout << "thread-2," << "cnt->" << cnt << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid1, tid2;pthread_create(&tid1, nullptr, routine1, nullptr);pthread_create(&tid2, nullptr, routine2, nullptr);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

说明:分别创建两个线程, 分别对全局变量进行访问,因为线程共享地址空间,所以一个线程修改了全局变量,另一个也会看到。

如果,我们想让每一个线程独立掌握该数据的控制权,线程只修改自己对该数据的副本进行修改,不想影响其他线程,就可以对该全局数据进行__thread修饰。

__thread int cnt = 0;

注意事项:

  • 线程局部存储,只能存储内置类型和部分指针 
  • 频繁地创建和销毁线程局部存储键或者频繁地访问线程局部存储变量可能会带来一定的性能开销。因为系统需要为每个线程管理这些线程局部存储的数据空间,并且在访问时需要进行额外的查找操作来获取当前线程对应的数据副本。
  • 虽然线程局部存储为每个线程提供了独立的数据副本,但如果多个线程之间通过指针或者其他方式间接访问到对方的线程局部存储数据,则仍然可能会出现线程安全问题。所以在使用 TLS 时,要确保线程之间的数据访问是正确的,避免出现数据竞争等情况。

七.封装线程库  

封装线程库时,我们期望在构造的时候就启动新线程,并执行上层所指定的任务。这里的入口函数在实现时不能直接作为普通成员函数,普通成员函数有一个隐藏的this指针,routine的参数就不符合要求了。

所以,我们要将入口函数定义为静态成员函数,这样就没有了this指针的影响力了。因为构造时需要用户传入一个任务,所以我们可以先对返回值为void,无参可调用对象进行包装,并作为成员。这样,我们就可以直接在routine中调用该成员即可。但是因为没有this指针,所以routine无法直接使用成员。这下,我们就可以利用pthread_create的最后一个参数了,该参数最后会传给routine,所以我们将this作为最后一个参数就可以传给routine了。

我们可以使用下面的接口设置和获取线程的名字,以往我们直接启动程序,所有的线程名都是一样的,当我们设置线程名后,就可以看到变化了.方便debug。

NAMEpthread_setname_np, pthread_getname_np - set/get the name of a threadSYNOPSIS#define _GNU_SOURCE             /* See feature_test_macros(7) */#include <pthread.h>int pthread_setname_np(pthread_t thread, const char *name);int pthread_getname_np(pthread_t thread,char *name, size_t len);
#ifndef __THREAD__HPP__
#define __THREAD__HPP__#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
#include <cstring>
#include <pthread.h>namespace MyThread
{static uint32_t cnt = 1;class Thread{private:using func_t = std::function<void()>;static void* routine(void* args){// 静态成员变量没有this指针,无法访问类内成员Thread* self = static_cast<Thread*>(args);pthread_setname_np(self->_tid, self->_name.c_str());self->_func();return nullptr;}public:// 创建线程Thread(func_t func):_isDetach(false),_isRunning(true),_func(func){_name = "thread-" + std::to_string(cnt++);int n = pthread_create(&_tid, nullptr, routine, (void*)this);if(n != 0){std::cerr << "create thread error" << strerror(n) << std::endl;return;}std::cout << "create thread success!" << std::endl;}~Thread(){}// 回收线程bool Join(){// Join线程时,线程必须是joinableif(_isDetach){std::cout << "thread is detached, can not be join" << std::endl;return false;}int n = pthread_join(_tid, nullptr);if(n != 0){std::cerr << "join thread error" << strerror(n) << std::endl;return false;}std::cout << "join thread success!" << std::endl;return true;}// 终止线程bool Stop(){if(_isRunning){int n = pthread_cancel(_tid);if(n != 0){std::cerr << "stop thread error" << strerror(n) << std::endl;return false;}std::cout << "stop thread success!" << std::endl;_isRunning = false;return true;}std::cout << "thread has stopped" << std::endl;return false;}// 分离线程bool Detach(){if(_isDetach){std::cout << "thread is detached" << std::endl;return false;}int n = pthread_detach(_tid);if(n != 0){std::cerr << "detach thread error" << std::strerror(n) << std::endl;return false;}std::cout << "detach thread success!" << std::endl;_isDetach = true;return true;}private:pthread_t _tid;std::string _name;bool _isDetach;bool _isRunning;func_t _func;};}#endif

我们还可以引入模板,在指定任务时,指定任务参数为任意类型,这样就可以从主线程中获取数据,来进行执行,同时也可以用于处理新线程的执行结果。

相关文章:

【Linux】线程

一.线程概念 我们在学习进程的时候已经知道了&#xff0c;进程内核数据结构pcb自己的代码和数据。那么单单一个task_struct是什么呢&#xff1f; 我们将单个的task_struct叫做轻量级进程&#xff0c;而这个轻量级进程也叫做线程。以往我们在了解进程的时候&#xff0c;一个进…...

WPF-遵循MVVM框架创建图表的显示【保姆级】

文章速览 1、技术栈实现步骤1、创建WPF工程项目2、引入框架 Caliburn.Micro、数据可视化库ScottPlot.WPF3、创建文件夹&#xff0c;并创建相应的View & ViewModel4、创建启动类5、将启动类设置为启动项6、编写View7、编写VM8、将VM和View中的图表进行绑定9、备注 示例效果 …...

深入详解人工智能数学基础—概率论-KL散度在变分自编码器(VAE)中的应用

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用…...

《代码整洁之道》第9章 单元测试 - 笔记

测试驱动开发 (TDD) 是一种编写整洁代码的“规程”或“方法论”&#xff0c;而不仅仅是测试技术。 JaCoCo 在运行测试后生成详细的覆盖率报告的工具&#xff0c; maven 引用。 测试驱动开发 测试驱动开发&#xff08;TDD&#xff09;是什么&#xff1f; TDD 不是说写完代码…...

每日c/c++题 备战蓝桥杯(P2392 kkksc03考前临时抱佛脚)

【题解】期末考试抱佛脚最短时间&#xff08;动态规划 | 二进制背包&#xff09; 题目链接 题目背景 kkksc03 的大学生活非常颓废&#xff0c;临近期末考试才开始疯狂复习。他有 4 门科目需要复习&#xff0c;每一科都有若干道题目&#xff0c;每道题目需要一定的时间完成。…...

徽客松S1 | 合肥首场 AI 黑客松招募

越来越多的黑客松在各个城市出现&#xff01;5 月 10 日&#xff0c;合肥&#xff0c;12 小时极速挑战。 我们和本次「徽客松」发起人 SDL 也是在一个黑客松上相识。当你的城市还没有黑客松可参加&#xff0c;与其等待&#xff0c;不如学习 SDL&#xff0c;自己发起一个&#…...

单片机-89C51部分:6、按键

飞书文档https://x509p6c8to.feishu.cn/wiki/EtkHw8MG0ipz3NkHlZEcwpEnn4g 一、应用场景&#xff1a; 轻触开关、按键、电容开关、光栅传感器、微动、关电开关 二、原理&#xff1a; 轻触按键可以理解为两根导线&#xff0c;按下时导线连接&#xff0c;松开时导线断开。我们可…...

小结: DHCP

交换机的物理接口分批地址池、全局分配地址池 分批地址池&#xff08;接口地址池/局部分配&#xff09; 按物理接口&#xff08;如 VLAN 接口、SVI、物理端口&#xff09;划分&#xff0c;每个接口单独配置一个小型地址池。适合规模较小、子网划分清晰的场景。配置方法示例&…...

matlab simulink中理想变压激磁电流容易有直流偏置的原因分析。

simulink把线性变压器模块拉出来&#xff0c;设置没有绕线电阻的变压器&#xff0c;激磁电感和Rm都有&#xff0c;然后给一个50%占空比的方波&#xff0c;幅值正负10V&#xff0c;线路中设置一个电阻&#xff0c;模拟导线阻抗。通过示波器观察激磁电流&#xff0c;发现电阻越小…...

国产三维CAD皇冠CAD在「通用设备制造业」建模教程:台式起重机

在制造业数字化转型的浪潮中&#xff0c;三维CAD软件已成为装备设计的核心工具&#xff0c;而国产软件的崛起正悄然改变行业格局。皇冠CAD&#xff08;CrownCAD&#xff09;作为中国自主研发的云端三维CAD平台&#xff0c;凭借全栈可控的底层架构、高效协同的设计流程及复杂场景…...

Day 12

文件操作 文件文件操作文件函数课堂笔记 文件 1&#xff09;概述 FILE 所有平台的名字都一样&#xff0c;FILE 是一个结构体类型,里面的成员功能一样,不同平台成员的名字不一样。 FILE *fp 1、fp指针&#xff0c;只用调用了fopen().在堆区分配空间,把地址返回给fp 2、fp指针…...

Lua 第11部分 小插曲:出现频率最高的单词

在本章中&#xff0c;我们要开发一个读取并输出一段文本中出现频率最高的单词的程序。像之前的小插曲一样&#xff0c;本章的程序也十分简单但是也使用了诸如迭代器和匿名函数这样的高级特性。 该程序的主要数据结构是一个记录文本中出现的每一个单词及其出现次数之间关系的表。…...

自然语言处理之机器翻译:注意力机制在低资源翻译中的突破与哲思

## 被忽视的7000种语言 在人工智能翻译技术突飞猛进的今天,一个残酷的事实被刻意掩盖:全球7000种语言中,超过95%缺乏构建现代机器翻译系统所需的基础资源。当我们在庆贺Transformer模型将英德翻译BLEU值推高至40%时,那些承载着人类文明基因的少数民族语言,正在经历着前所未…...

SQL 处理重复数据之技巧(Techniques for Handling Duplicate Data with SQL)

SQL 处理重复数据之技巧 ❝ 在日常数据库操作中&#xff0c;我们经常会遇到重复数据的问题。重复数据不仅会占用存储空间&#xff0c;还可能导致数据分析结果不准确。本文将详细讲解 SQL 中处理重复数据的常用方法&#xff0c;帮助你更高效地管理数据库中的数据。 一、为什么会…...

Redis01-基础-入门

零、文章目录 Redis01-基础-入门 1、认识 NoSQL NoSQL 知识请参考&#xff1a;https://blog.csdn.net/liyou123456789/article/details/132612444 2、认识 Redis &#xff08;1&#xff09;简介 Redis&#xff08;Remote Dictionary Server&#xff0c;远程字典服务&…...

辞九门回忆

2025年月日&#xff0c;13~30℃&#xff0c;挺好的 待办&#xff1a; 《高等数学2》期末试卷 高数重修电子版材料 冶金《物理》期末试卷 《物理[2]》期末试卷 批阅冶金《物理》作业→→统计平时成绩 遇见&#xff1a;遇见一位小姐姐。 感受或反思&#xff1a;不主动推动关系&a…...

全球城市范围30米分辨率土地覆盖数据(1985-2020)

Global urban area 30 meter resolution land cover data (1985-2020) 时间分辨率年空间分辨率10m - 100m共享方式保护期 277 天 5 时 42 分 9 秒数据大小&#xff1a;8.98 GB数据时间范围&#xff1a;1985-2020元数据更新时间2024-01-11 数据集摘要 1985~2020全球城市土地覆…...

java编程式、声明式事务简单介绍

大家吼鸭&#xff01;今天学习新项目的时候&#xff0c;项目中运用了编程式项目&#xff0c;有点不理解什么叫编程式事务&#xff0c;于是我去查询了一些资料&#xff0c;大概了解了一下。现在做一个简单的介绍。 编程式事务和声明式事务的区别 现在想象一个场景&#xff0c;…...

Golang 遇见 Kubernetes:云原生开发的完美结合

Golang 和 Kubernetes 简介 Golang 概述 Golang&#xff0c;也称为 Go&#xff0c;是由 Google 开发的一种开源编程语言。Go 由 Robert Griesemer、Rob Pike 和 Ken Thompson 设计&#xff0c;于 2009 年首次发布&#xff0c;此后在各个领域都获得了广泛的关注&#xff0c;尤其…...

第三章,GRE和MGRE

VPN---虚拟专用网络 VPN的核心技术----隧道技术---封装 GRE---通用路由封装 配置 GRE的配置&#xff1a; R1&#xff1a; [r1]interface Tunnel 0/0/0 ---创建一个虚拟的隧道接口 [r1-Tunnel0/0/0]ip address 192.168.3.1 24 ---给隧道接口分配一个IP地址 [r1-Tunnel0/0/0]t…...

redis常用集合操作命令

在 Redis 的命令行界面&#xff08;redis-cli&#xff09;中&#xff0c; Redis 的集合&#xff08;Set&#xff09;是无序的&#xff0c;且集合中的元素是唯一的。Redis 本身没有直接提供获取集合中某个特定属性的命令&#xff0c;因为集合中的元素是简单的值&#xff0c;而不…...

vue3中ref在js中为什么需要.value才能获取/修改值?

文章目录 [TOC](文章目录) 一、ref定义值什么情况下需要.value1. 情况1:在js中需要使用.value2. 情况2:在html模版中不需要使用.value3. 情况31.代码2.效果3. 二、重新了解一下vue2和vue3的响应式1.vue2&#xff08;Object.defineProperty&#xff09;2.vue3&#xff08;proxy&…...

使用vue2 开发一个纯静态的校园二手交易平台-前端项目练习

这篇文章给大家分享一个适合练习学习前端技术的项目&#xff1a;校园二手交易平台系统。 因为最近在学习vue相关的技术&#xff0c;所以就根据学习的前端技术&#xff0c;来写一些纯前端的项目来练习&#xff0c;这篇文章主要是分享一下 我做的这个项目的一些功能&#xff0c;如…...

使用wavesurferJs实现录音音波效果

效果图展示 插件安装 npm i wavesurfer实现过程 <!-- author: weileiming date: 2025-04-26 14:04:08 description: 悬浮音波层 props:isRecord: 录制状态waveOptions: 音波基础配置overlayStyle: 基础蒙层配置 methods:togglePlay: 切换录制状态 --> <template>…...

Golang 类型方法

在 Go 语言中&#xff0c;方法绑定到任意类型的特性可以称为 “类型方法&#xff08;Type Methods&#xff09;” 或 “接收者方法&#xff08;Receiver Methods&#xff09;”&#xff0c;它体现了以下几种核心编程思想&#xff1a; 1. 官方术语&#xff1a;接收者方法&#x…...

多模态常见面试题

多模态常见面试 一、最近关注的论文&#xff0c;多模态视觉大模型(CLIP,DALLE)&#xff1f;二、blip2的架构&#xff0c;优势和之前多模态模型的区别&#xff1f;三、多模态融合后&#xff0c;怎样知道最终结果受哪种模态影响更大&#xff1f;四、多模态中常见的SOTA模型有哪些…...

LangChain构建大模型应用之RAG

RAG&#xff08;Retrieval-augmented Generation 检索增强生成&#xff09;是一种结合信息检索与生成模型的技术&#xff0c;通过动态整合外部知识库提升大模型输出的准确性和时效性。其核心思想是在生成答案前&#xff0c;先检索外部知识库中的相关信息作为上下文依据&#xf…...

Git 全面解析:从核心概念到生态应用

Git 一、Git 起源与定位 诞生背景&#xff1a;2005 年由 Linus Torvalds 为管理 Linux 内核开发而设计&#xff0c;因 BitKeeper 许可证争议&#xff0c;急需分布式版本控制系统&#xff08;DVCS&#xff09;替代集中式工具&#xff08;如 SVN&#xff09;。核心优势&#x…...

国产免费工作流引擎star 5.9k,Warm-Flow版本升级1.7.0(新增大量好用功能)

国产免费工作流引擎star 5.9k&#xff0c;Warm-Flow版本升级1.7.0&#xff08;新增大量好用功能&#xff09; 主要更新内容项目介绍功能思维导图设计器流程图演示地址官网Warm-Flow视频 之前大家一直吐槽没有撤销、驳回到上一个任务和拿回等功能&#xff0c;此次版本全都带给大…...

camera知识学习

1、DSP DSP&#xff08;数字信号处理器&#xff09;&#xff0c;这个是介于sensor和ISP处理的一个处理阶段&#xff0c;会进行一些传感器方面的偏硬件处理&#xff0c;再进行数据格式的转换&#xff0c;将raw数据转换成RGB数据或者YUV数据...

Java高频常用工具包汇总

Java高频常用工具包汇总 Java生态系统中有许多广泛使用的工具包&#xff0c;以下是一些高频常用的工具包分类汇总&#xff1a; 1. 核心工具包 Apache Commons系列 Commons Lang - 提供各种基础工具类Commons IO - 文件/IO操作工具Commons Collections - 集合扩展工具Commons …...

蓝桥杯 16. 密文搜索

密文搜索 原题目链接 题目描述 福尔摩斯从 X 星收到一份资料&#xff0c;全部是小写字母组成。 他的助手提供了另一份资料&#xff1a;许多长度为 8 的密码列表。 福尔摩斯发现&#xff0c;这些密码是被打乱后隐藏在先前那份资料中的。 请你编写一个程序&#xff0c;从第…...

Spring Boot 中多线程的基础使用

1. 核心机制 Spring Boot 通过 TaskExecutor 和 Async 注解支持多线程编程&#xff0c;结合线程池管理&#xff0c;有效提升应用性能。核心组件包括&#xff1a; EnableAsync&#xff1a;启用异步任务支持。 Async&#xff1a;标记方法为异步执行。 ThreadPoolTaskExecutor&…...

660SJBH企业信息管理系统

第一章 问题来源 1.1 课题提出背景和意义 由于企业规模进一步扩大&#xff0c;企业信息的管理也变得越来越复杂。为此&#xff0c;切实有效的把企业信息管理系统引入企业管理领域中&#xff0c;对于促进企业管理制度和提高企业质量有着显着意义。 Internet的发展使我们的企业…...

OpenCV实验室工具的使用

OpenCV实验室工具是一个调用OpenCV常见函数&#xff0c;让用户调整参数&#xff0c;快速得到试验结果的工具软件。 软件界面中包含三列&#xff0c;第一列是功能菜单&#xff0c;第二列是实现某一功能时需要输入的参数&#xff0c;第三列是图像处理历史。 OpenCV实验室包含了常…...

月之暗面开源-音频理解、生成和对话生成模型:Kimi-Audio-7B-Instruct

一、Kimi - Audio 简介 Kimi - Audio 是一个开源的音频基础模型&#xff0c;在音频理解、生成和对话等方面表现出色。其设计旨在作为一个通用的音频基础模型&#xff0c;能够在单一统一的框架内处理各种音频处理任务&#xff0c;如语音识别&#xff08;ASR&#xff09;、音频问…...

依赖于切片级标签,结合信息瓶颈理论,对弱监督病理切片分类模型进行微调

小罗碎碎念 在医学AI领域&#xff0c;病理全切片图像&#xff08;WSI&#xff09;分析意义重大&#xff0c;但面临诸多难题。 高分辨率的WSI使得获取精确注释极为困难&#xff0c;且计算成本高昂。 多实例学习&#xff08;MIL&#xff09;虽能利用WSI级弱监督缓解注释压力&…...

UE5 NDisplay 单主机打包运行

前言 最近在做UE的左右眼双屏输出&#xff0c;找了半天只有近年来比较火的NDispaly可以做这件事了&#xff0c;看了一下官方的教程写的很全面&#xff0c;但是相对笼统了一些&#xff0c;发现B站和一些博客了也写了有&#xff0c;但是我建议还是好好过一遍官方文档吧&#xff0…...

Kubernetes/KubeSphere 安装踩坑记:从 context deadline exceeded 到成功部署的完整排障笔记

目录 Kubernetes/KubeSphere 安装踩坑记&#xff1a;从 context deadline exceeded 到成功部署的完整排障笔记 一、问题现象 二、第一手日志采集 三、定位思路 四、分步解决 4-1 处理 pause:3.8 4-2 处理 kube-apiserver:v1.31.0 五、再次安装并验证 六、经验总结 七…...

SpringMVC 静态资源处理 mvc:default-servlet-handler

我们先来看看效果,当我把这一行注释掉的时候&#xff1a; 我们来看看页面&#xff1a; 现在我把注释去掉&#xff1a; 、 可以看到的是&#xff0c;这个时候又可以访问了 那么我们就可以想&#xff0c;这个 <mvc:default-servlet-handler />它控制着我们页面的访问…...

2、Linux操作系统下,ubuntu22.04版本安装搜狗输入法

1.添加中文语言支持&#xff0c;打开此窗口的步骤如下&#xff1a; system setting>language and region>language>install/remove language&#xff0c;之后弹出下面的窗口&#xff0c;点击“reminder me later勾选Chinese&#xff08;simplified&#xff09;&#…...

go语言八股文(四)

1.go语言中defer的变量快照在什么情况下会生效 1. 变量在 defer 被注册时的值被捕获 当 defer 被注册时&#xff0c;它会捕获变量在那一刻的值。如果变量是值类型&#xff08;如基本类型、结构体等&#xff09;&#xff0c;defer 会捕获该值的副本&#xff1b;如果变量是指针类…...

烽火HG680-MC_晨星MSO9385芯片-2+8G_安卓9.0_不分地区通刷卡刷固件包

烽火HG680-MC_晨星MSO9385芯片-28G_安卓9.0_不分地区通刷卡刷固件包 刷机教程&#xff1a; 1、准备一个优盘卡刷强刷刷机&#xff0c;用一个usb2.0的8G以下U盘&#xff0c;fat32&#xff0c;2048块单分区格式化&#xff08;强刷对&#xff35;盘非常非常挑剔&#xff0c;usb2.…...

秒杀压测计划 + Kafka 分区设计参考

文章目录 前言&#x1f680; 秒杀压测计划&#xff08;TPS预估 测试流程&#xff09;1. 目标设定2. 压测工具推荐3. 压测命令示例&#xff08;ab版&#xff09;4. 测试关注指标 &#x1f4e6; Kafka Topic 分区设计参考表1. 单 Topic 设计2. 分区路由规则设计&#xff08;Part…...

跨境电商货物体积与泡重计算器:高效便捷的物流计算工具

跨境电商货物体积与泡重计算器&#xff1a;高效便捷的物流计算工具 工具简介 货物体积与泡重计算器是一款免费的在线工具&#xff0c;专门为物流从业者、跨境电商卖家和需要计算货物运输体积重量的用户设计。这款工具可以帮助您快速计算货物的体积和对应的空运、快递泡重&…...

隧道代理ip的优势

日益复杂的互联网环境中&#xff0c;爬虫技术已经成为大数据不可或缺的一环。提到代理IP&#xff0c;大部分人首先想到的是普通的静态IP或动态代理IP&#xff0c;然而&#xff0c;隧道代理IP――这一更为高效、灵活的选择&#xff0c;在许多场景中能为开发者们提供绝佳的技术支…...

Selenium自动化测试+OCR-获取图片页面小说

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 随着爬虫技术的发展&#xff0c;反爬虫技术也越来越高。 目前有些网站通过自定义字体库的方式实现反爬&#xff0c;主要表现在页面数据显示正常&#xff0c;但是…...

MySQL 锁等待超时问题解析:Lock wait timeout exceeded;try restarting transaction

目录 一、问题背景二、问题原因三、解决方案1. 重启事务2. 优化事务管理3. 调整锁等待超时设置4. 分析并优化锁竞争5. 查找并终止持有锁的操作6. 优化 SQL 语句四、预防措施五、总结在使用 MySQL 数据库时, Lock wait timeout exceeded;try restarting transaction 这个错误…...

学习笔记2(Lombok+算法)

Lombok &#xff1a; 介绍&#xff1a; Lombok 是一个在 Java 开发中广泛使用的开源库&#xff0c;它的主要作用是通过注解的方式&#xff0c;减少 Java 代码中大量的样板代码&#xff08;如 getter、setter、构造函数等&#xff09;&#xff0c;从而让代码更加简洁、易读和易…...

【音视频】SDL简介

官网&#xff1a;官网 文档&#xff1a;文档 SDL&#xff08;Simple DirectMedia Layer&#xff09;是一套开放源代码的跨平台多媒体开发库&#xff0c;使用C语言写成。SDL提供数种控制图像、声音、输出入的函数&#xff0c;让开发者只 要用相同或是相似的代码就可以开发出跨多…...