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

【Linux】线程的概念与控制

目录

1. 整体学习思维导图

2. 线程的概念

2.1 基础概念

2.2 Linux下的线程 

初步理解:

2. 分页式存储

3.1 页表

3.1.1 页框/页

3.1.2 页表机制

3.1.3 从虚拟地址到物理地址的转换

总结:

3.2 TLB快表

3.3 缺页异常(Page Fault):

4. 线程理解

4.1 线程优点

4.2 线程缺点

4.3 线程和进程

4.3.1 线程的函数共享

​编辑 4.3.2 线程共享全局变量

5. 线程控制

POSIX 线程库

5.1 创建一个线程

见一见 C++11 的线程库

​编辑 Create + join + 获取返回值

5.2 线程终止

5.3 线程分离

5.4 多线程练习

6. 线程ID及进程地址空间布局

7. 线程封装


1. 整体学习思维导图

2. 线程的概念

2.1 基础概念

  • 教材上对于进程/线程的解释(概念角度):

    • 进程 = PCB + 数据代码(执行流)

    • 线程:是进程内部的一个执行分支(执行流)

  • 从内核和资源角度:

    • 进程:承担资源分配的基础实体

      • 为什么这么说?进程创建可能会让磁盘文件加载到物理内存,申请需要使用的空间资源,构建映射表等等,这一切都需要使用申请到内部的资源,进程管理使用这部分资源。

    • 线程:CPU调度的基本单位

      • 为什么?通过Linux下具体的线程设计来理解。

2.2 Linux下的线程 

  1. 进程访问绝大多数资源时是通过地址空间进行访问的,地址空间就是访问资源的"窗口",有了这个"窗口"我们就可以通过其看到内部的资源数据,地址空间的核心是由虚拟地址到物理地址的映射(MMU负责-CPU内部)。

  2. 现在我们创建线程时,多个task_struct共享这个"窗口"(地址空间),并且将资源分配给他们,这样我们就通过进程模拟实现出线程了。

  3. 对于Linux系统来说,不论你是线程还是进程都是一律看做为执行流,只需执行处理对于的代码和数据即可,你的mm_struct由一个task_struct管理我们叫做进程,由多个管理我们叫做线程,因此执行流 <= 进程的,Linux中将一个个的task_struct(线程)我们叫做轻量级进程(LWP)。

    1. 单进程(传统进程):一个task_struct + 独立mm_struct

    2. 线程组(多线程):多个task_struct + 共享同一个mm_struct

  4. 对于CPU来说,在其内部有一个运行队列run_queue,不断的计算处理一个个task_struct的代码和数据,这也就是为什么说线程是CPU的最小调度单位,CPU只需要处理这一个个轻量级进程(LWP)即可!

初步理解:

  • 结论一:Linux的"线程"可以采用进程进行模拟实现。

  • 结论二:对内存资源划分本质就是对虚拟地址的划分,因此虚拟地址就是内存资源的代表。

  • 结论三:代码区怎么划分,以函数划分,函数的入口地址就是虚拟地址的集合,让未来不同的线程执行ELF文件中的不同函数即可。

  • 结论四:在不同于Linux的平台,如Windows下对于线程的描述与管理重新设计了TCB,而一个进程中组织着需要使用到的TCB线程数组。Linux的选择是复用进程的代码与结构设计线程,因此Linux的线程就是轻量级进程(LWP),或者说由轻量级进程(LWP)实现。

  • 结论五:

    • 进程强调独占,部分资源共享(如通信)。

    • 线程强调共享,部分独占。

2. 分页式存储

分页式存储是操作系统管理内存的核心技术之一,旨在解决内存碎片化问题并提升资源利用率。

如果在没有虚拟内存和分页机制的情况下,每⼀个用户程序在物理内存上所对应的空间必须是连续的,如下图:

不同程序退出时间和内存占用大小的差异,会导致空闲内存被分割为不连续的小块(外部碎片化)。而当分配的内存块大于实际需求时,则会产生内部碎片化。

3.1 页表

3.1.1 页框/页

我们知道磁盘被划分为4KB为单位的数据块,磁盘的数据(可执行程序等等)需要加载到物理内存,这就需要物理内存块也划分为4KB的内存块方便IO以4KB为单位进行交换!而将物理内存以4KB划分后的每一块我们叫做页框!

把物理内存按照⼀个固定的长度的页框进行分割,有时叫做物理页。每个页框包含⼀个物理页(page)。一个页的大小等于页框的大小。大多数 32位 体系结构支持 4KB 的页,而 64位 体系结构⼀般会支持 8KB 的页。区分一页和一个页框是很重要的:

页框是⼀个存储区域;

而页是⼀个数据块,可以存放在任何页框或磁盘中

现在设定我们的物理内存大小为4GB, 4GB/4KB = 1048576个页框这么多页框都是由操作系统划分的,那么操作系统就需要对这些页框进行管理!先描述,在组织!

struct page mem[1048576];
/* 每一个page都有下标!那么起始物理地址就知道了 */
/* 具体的物理地址 = 起始物理地址 + 页内(4KB)偏移 */
struct page_info {unsigned short flags;unsigned short count;struct inode * inode;unsigned long offset;struct page_info * next_same_inode;struct page_info * prev_same_inode;struct page_info * next_hash;struct page_info * prev_hash;struct wait_queue *wait;
};

进程在申请物理内存的时候是在干什么?

  • 查数组,改page结构体中的内容!

  • 建立内核数据结构的对应关系!

3.1.2 页表机制

我们前面了解过页表是虚拟地址和物理地址的映射表,假设我们每一个地址都需要映射,在 32 位系统中,地址的长度是 4 个字节,那么页表中的每⼀个表项就是占用 4 个字节。所以页表占据的总空间大小就是: 1048576*4 = 4MB 的大小。也就是说映射表自己本身,就要占用 4MB / 4KB = 1024 个物理页, 这显然太多了,因此页表绝对不是一张单独的页表!

页目录-页表机制

  • 这里的每⼀个表,就是真正的页表,所以⼀共有 1024 个页表。⼀个页表自身占用 4KB ,那么1024 个页表⼀共就占用了 4MB 的物理内存空间,和之前没差别啊?

  • 从总数上看是这样,但是⼀个应用程序是不可能完全使用全部的4GB空间的,也许只要几十个页表就可以了。例如:⼀个用户程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使用 3 个页表就足够了。

  • 页目录结构管理着这1024个页表,所以操作系统在加载用户程序的时候,不仅仅需要为程序内容来分配物理内存,还需要为用来保存程序的页目录和页表分配物理内存 。

3.1.3 从虚拟地址到物理地址的转换

在32位机器下,4字节的虚拟地址共有32个比特位,前10位比特位用于表示页目录的起始地址,中间10比特位用于表示页表的起始地址,最后12位用于表示页框的偏移量:

  • 页目录索引(前 10 位):页目录是用来存放页表起始地址的一张表。前 10 位比特位能表示2^10个不同的索引,也就是可以指向2^10个不同的页表。每个页表的起始地址存于页目录的对应项里。

  • 页表索引(中间 10 位):页表是用来存放页框起始地址的一张表。中间 10 位比特位能够表示2^10个不同的索引,也就是可以指向2^10个不同的页框。每个页框的起始地址存于页表的对应项中。

  • 页内偏移(最后 12 位):最后 12 位比特位能够表示2^12个不同的偏移量。因为2^12=4096字节,也就是 4KB,所以通常页的大小是 4KB。借助页内偏移,可以在一个页框内定位到具体的字节位置。

MMU硬件转换自动完成的两个阶段分别是:

  1. 查找到虚拟地址对应的页框

  2. 根据虚拟地址低12位,作为页内偏移,访问具体字节!

总结:

  1. 细节1:申请内存-->查找struct page mem[1048576]数组-->找到没有被使用的page-->page index-->物理页框地址!

  2. 细节2:写时拷贝,缺页中断,内存申请等等,背后都存在着建立新页表和新映射关系的操作!

  3. 细节3:进程:一张页目录+n张页表构建的映射体系,虚拟地址就是索引,物理地址页框就是索引的目标。虚拟地址的低12位 + 页框地址 = 物理地址!

  • 虚拟地址与资源:虚拟地址是程序执行流看到的资源代表,资源划分本质是虚拟地址空间划分,资源共享即虚拟地址共享。

  • 数据结构:mm_struct 结合 vm_area_struct 用于管理虚拟地址空间,统计资源数据并呈现整体信息。

  • 页表功能:页表作为 “虚拟到物理的地图”,实现虚拟地址到物理地址的映射,是内存管理的关键环节。

3.2 TLB快表

我们每次通过MMU进行一次页目录,页表到页框的查询就是一次IO操作,当我们大量去查询时就会带来效率问题,因此为了解决这个频繁的查询请求,我们将最近查询的结果在TLB中缓存起来,这样在查询之前MMU会去TLB查看是否存在,如果存在直接使用,如果没有再去查询后加入到TLB中。

3.3 缺页异常(Page Fault):

  • 定义:CPU 向 MMU 发送的虚拟地址,在 TLB 和页表中均无对应物理页时触发,是硬件中断触发、可通过软件逻辑纠正的错误。

  • 触发场景:目标内存页在物理内存中无对应物理页,或存在但无访问权限,导致 CPU 无法获取数据。

  • 处理流程:因 CPU 无数据无法计算,进程从用户态切换到内核态,缺页中断交由内核的 Page Fault Handler(缺页异常处理程序) 处理。

缺页中断处理程序(PageFaultHandler)处理的三种缺页类型:

  • Hard Page Fault(硬缺页错误 / 主要缺页错误):物理内存无对应物理页,需从磁盘读取数据到内存,再由 MMU 建立虚拟地址与物理地址的映射。

  • Soft Page Fault(软缺页错误 / 次要缺页错误):物理内存存在对应物理页(其他进程调入),仅需 MMU 建立映射,无需磁盘读取,常见于多进程共享内存区域场景。

  • Invalid Page Fault(无效缺页错误):因进程越界访问内存、解引用空指针等非法操作触发,会报segment fault错误并中断进程。

4. 线程理解

  • 线程资源划分:线程划分资源时,通过划分虚拟地址空间获取合法虚拟地址,而页表是虚拟地址到物理地址的映射载体,划分页表本质就是管理虚拟地址空间。

  • 线程资源共享:地址空间共享的核心是页表条目的共享。页表条目记录虚拟地址与物理资源(如内存页框)的映射关系,共享页表条目意味着多个线程可通过相同虚拟地址访问同一物理资源,实现资源共享。

4.1 线程优点

  1. 创建新的线程比进程的代价小,一个线程只需在原有进程的基础上创建一个PCB并且进行对应的资源划分即可。

  2. 线程与进程切换相比较:

    1. 虚拟内存映射机制:

      1. 线程属于同一进程,共享进程虚拟内存空间。切换时,因虚拟地址空间不变,无需重新构建页表等地址映射关系。

      2. 进程拥有独立虚拟地址空间。进程切换时,新进程的虚拟地址空间与原进程不同,必须重新加载页目录、重建页表映射等,开销更大。

    2. 上下文切换内容:

      1. 线程切换:主要保存 / 恢复线程独有的上下文,如寄存器值、栈指针、线程状态等,不涉及虚拟内存空间的重新初始化。

      2. 进程切换:除了保存 / 恢复通用寄存器、程序计数器等基础上下文,还需处理虚拟内存相关资源(如页表、内存管理数据结构),以及进程独有的资源(如文件描述符状态等),切换成本显著高于线程。进程切换会导致TLBCache失效,下次需要重新加载!

注:Cache 硬件是一种高速存储器。

  1. 线程占用的资源少于进程,并且线程可以充分发挥CPU并行的作用。

4.2 线程缺点

  1. 异常方面

    1. 如一个单个线程出现异常,如除0,访问野指针错误,那么其他线程和当前进程都会崩溃!

/* 1. 线程异常 */void *routinue1(void *args)
{int cnt = 3;while(cnt--){std::cout << "我是新线程, 我的name:" << (char*)args << std::endl;sleep(1);}/* 野指针操作--> 异常 */int* p = nullptr;*p = 100;return nullptr;
}void *routinue2(void *args)
{int cnt = 3;while(cnt--){std::cout << "我是新线程, 我的name:" << (char*)args << std::endl;sleep(1);}return nullptr;
}int main()
{/* 创建两个线程 */pthread_t tid1, tid2;pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");while(true){std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

  1. 资源方面

    1. 多个线程彼此是可以使用其他线程的资源的,只需要有一个可以看见其他线程资源的窗口,具有线程不安全性。

  2. 健壮性方面

    1. 线程之间缺乏保护机制,一个线程的错误可能会波及到其他线程,导致整个系统的健壮性下降。

4.3 线程和进程

  • 进程:作为资源分配基本单位,系统为进程分配内存、文件描述符等资源。

  • 线程:作为调度基本单位,线程切换开销低于进程。线程共享进程数据(如全局变量、内存空间),同时拥有私有数据:

    • 线程 ID:唯一标识线程。

    • 一组寄存器:保存线程运行时的寄存器状态,用于上下文切换。

    • 栈:线程独立的调用栈,存储局部变量等。

    • errno:记录线程错误信息。

    • 信号屏蔽字:线程对信号的屏蔽设置。

    • 调度优先级:决定线程调度顺序的优先级参数。

4.3.1 线程的函数共享

/* 2. 线程函数共享 */
/* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}/* 可重入函数 */
void *routinue(void *args)
{std::string name = static_cast<char*>(args);while(true){sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << std::endl;}return nullptr;
}int main()
{/* 创建两个线程 */pthread_t tid1, tid2;pthread_create(&tid1, nullptr, routinue, (void*)"thread-1");pthread_create(&tid2, nullptr, routinue, (void*)"thread-2");while(true){std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

 4.3.2 线程共享全局变量

/* 3. 线程全局变量共享 */
/* 将tid转换为16进制 */
int val = 100;
std::string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}/* 可重入函数 */
void *routinue1(void *args)
{std::string name = static_cast<char*>(args);while(true){sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(会修改):" << val << std::endl;++val;}return nullptr;
}/* 可重入函数 */
void *routinue2(void *args)
{std::string name = static_cast<char*>(args);while(true){sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(观察):" << val << std::endl;}return nullptr;
}int main()
{/* 创建两个线程 */pthread_t tid1, tid2;pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");while(true){std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

  • 刚刚上面演示了 一个全局变量被多线程共享,那么如果我们想让多个线程各自私有这一个变量,该怎么做呢? --> g++有一个编译选项 __thread

  • 用__thread修饰这个全局变量即可。运行后,主线程和新线程gval的地址也不一样了。这种情况叫线程的局部存储,原始代码里只看到一个gval,但是他们用的是各自的gval

  • 这种情况只在Linux中有效。__thread只能用来修饰内置类型。

// 线程局部存储
// 编译型关键字,给每个线程来一份
// 虽然用的是同一份值和变量名,但是编译的时候把它地址编程不同
// 此时就叫做线程局部存储
// 注意:__thread 只能修饰内置类型
__thread int val = 100; // 此时主线程和新线程看到的地址不同/* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}/* 可重入函数 */
void *routinue1(void *args)
{std::string name = static_cast<char*>(args);while(true){sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(会修改):" << val << std::endl;++val;}return nullptr;
}/* 可重入函数 */
void *routinue2(void *args)
{std::string name = static_cast<char*>(args);while(true){sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(观察):" << val << std::endl;}return nullptr;
}int main()
{/* 创建两个线程 */pthread_t tid1, tid2;pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");while(true){std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;sleep(1);}pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

5. 线程控制

POSIX 线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的

  • 要使用这些函数库,要通过引入头文件<pthread.h>

  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

5.1 创建一个线程

功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数: 
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数,这个参数可以是对象也可以是string等等
返回值:成功返回0;失败返回错误码  
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>void *ThreadRun(void *args)
{std::string name = (char*)args;while(true){std::cout << "我是新线程,我们名称是" << name << std::endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRun, (void*)"Thread-1");while(true){std::cout << "我是主线程" << std::endl;sleep(1);}return 0;
}

由于在Linux中没有线程的概念,只存在轻量级进程(LWP),因此需要封装一个线程库,这个线程库我们需要动态链接! 

Thread:Thread.ccg++ -o $@ $^ -lpthread
.PHONY:clean
clean:rm -f Thread

  • 查看当前系统中存在的线程状态:

ps -aL

 我们可以看见主线程和新线程的PID都是一致的,这说明他们属于同一个进程,LWP则是线程在CPU中调度识别的标识符,这边注意LWP和tid是不同的两个东西!

这个函数是获取线程ID的,这个ID是由封装库pthread维护的和底层内核CPU调用线程所用识别不同线程LWP是不一样的!

线程创建运行完之后有点像僵尸进程一样需要主线程进行等待,目的也是为了防止内存泄漏! 

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数: thread:线程ID
retval:它指向⼀个指针,后者指向线程的返回值(二级指针是因为线程执行的函数返回值是void*, 防止形参是实参的拷贝需要传入二级指针获取)
返回值:
成功返回0;失败返回错误码  
  • 调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread join 得到的终止状态是不同的,总结如下:

  • 如果 thread 线程通过 return 返回,value ptr 所指向的单元里存放的是 thread 线程函数的返回值。

  • 如果 thread 线程被别的线程调用 pthread cancel 异常终掉,value_ptr 所指向的单元里存放的是常数PTHREAD CANCELED(void*(-1))

  • 如果 thread 线程是自己调用 pthread exit 终止的,value ptr 所指向的单元存放的是传给 pthread exit 的参数。

  • 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给 value_ptr 参数

见一见 C++11 的线程库

/* c++11 线程库 */
void *routinue()
{while(true){std::cout << "我是新线程,我的PID是 " << getpid() << std::endl;sleep(1);}
}int main()
{std::thread t(routinue);while(true){std::cout << "我是主线程" << std::endl;sleep(1);}t.join();return 0;
}  

c++11封装线程库是为了多平台的代码可移植性。 

 Create + join + 获取返回值

void *routinue(void *args)
{std::string name = (const char*)args;int cnt = 3;while(cnt--){std::cout << "我是一个新线程, 我的tid是:" << pthread_self() << std::endl;sleep(1);}return (void*)100;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routinue, (void*)"thread-1");void* ret = nullptr;sleep(5);pthread_join(tid, &ret);std::cout << (long long)ret << std::endl;return 0;
}

5.2 线程终止

我们终止一个线程而不终止整个进程有以下三种方法:

  1. 线程函数的return

  2. 线程调用pthread_exit终止自己。

  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

功能:线程终止
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:不要指向一个局部变量
返回值:无返回值,和进程一样,一旦结束无非返回调用它的调用者(本身)。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。 

/* 对象 */
class RetObject
{
public:RetObject(int a, int b):_a(a),_b(b){}int Excute(){return _a + _b;}~RetObject(){}
private:int _a;int _b;
};int ret = 0;void *routine(void *args)
{RetObject* ro = static_cast<RetObject*>(args);ret = ro->Excute();std::cout << ret << std::endl;pthread_exit((void*)ret);// return (void*)ret;
}int main()
{pthread_t tid;RetObject* ro = new RetObject(10, 20);pthread_create(&tid, nullptr, routine, (void*)ro);void *ret = nullptr;pthread_join(tid, &ret);std::cout << (long long)ret << std::endl;return 0;
}

功能:取消⼀个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数: thread:线程ID
返回值:成功返回0;失败返回错误码  
void *routine(void *args)
{while(true){std::cout << "我是一个新线程, 我的tid是:" << pthread_self() << std::endl;sleep(1);}return (void*)123;
}int main()
{pthread_t tid;RetObject* ro = new RetObject(10, 20);pthread_create(&tid, nullptr, routine, (void*)ro);pthread_cancel(tid);void *ret = nullptr;pthread_join(tid, &ret);std::cout << (long long)ret << std::endl;return 0;
}

  • 线程取消一个就join一个。由上图可知,线程被取消后,线程的退出结果是 -1

  • -1对应pthread库中的一个宏 #define PTHREAD_CANCELD ((void*)-1)

5.3 线程分离

  • 默认情况下新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,回收释放资源,防止系统内存泄漏。

  • 但是如果我们并不关心线程执行的返回值,我们能否直接让线程退出时自己释放自己的资源,如果要实现这个我们就需要分离线程。

  • 分离和joinable是对立的属性,一个线程只能是其中一种。

  • 如果一个线程被分离了,它就不需要join,但是线程依旧属于进程内部!

int pthread_detach(pthread_t thread);
int pthread_detach(pthread_self());
// 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
/* 线程分离 */
void *routine(void *args)
{std::cout << "我是新线程, 我的tid:" << pthread_self() << std::endl;std::cout << "我要开始分离了!" << std::endl;sleep(1);pthread_detach(pthread_self());return 0;
}
int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine, (void*)"thread-1");int n = pthread_join(tid, nullptr);std::cout << n << std::endl;return 0;
}

  • 线程自己把线程自己分离,主线程进入join 失败了然后退出,因此没有阻塞

  • 现在让主线程把线程分离,此时主线程也会退出,说明线程一旦被分离,就不能 Join 了

  • 主线程不会卡在 Join,而是会继续往后走,主线程结束了,整个进程就结束了。这样的话,假如我们还有其他线程,可能它们还没起来就死亡了

  • 所以分离线程后,主线程就可以做自己的事了,不用管新线程,即使新线程分离,只要分离的线程异常了,还是会影响整个进程。

  • 注意:在多执行流情况下,主执行流是最后退出的

5.4 多线程练习

/* 创建多线程 */
// /* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{char buffer[64];snprintf(buffer, sizeof(buffer), "0x%lx", tid);return buffer;
}/* 可重入函数 */
void *routinue(void *args)
{std::string name = static_cast<char*>(args);sleep(1);std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self())  << std::endl;return nullptr;
}int main()
{/* vector 管理起来线程tid */std::vector<pthread_t> v;for(int i = 0; i < 10; ++i){pthread_t tid;// char buffer[64]; // 会导致线程安全问题,多个线程访问的是同一资源,资源不一致char* buffer = new char[64];snprintf(buffer, 64, "thread-%d", i);pthread_create(&tid, nullptr, routinue, (void*)buffer);v.push_back(tid);}for(auto& e : v){int n = pthread_join(e, nullptr);if(n == 0)std::cout << "join success!" << std::endl;}return 0;
}

6. 线程ID及进程地址空间布局

Linux没有真正的线程,由轻量级进程模拟实现,OS提供的接口不易直接提供线程接口,而是封装轻量级进程形成用户需要使用的原生线程库!

 我们的进程是可执行程序,并且是ELF格式的,pthread.so是动态库,我们的可执行程序加载,形成进程,动态链接和动态地址重定向,并且要将动态库加载到内存,映射到当前进程的地址空间上!进程自己的代码区可以访问到pthread库内部的函数或者数据!

 创建线程要去调用映射的方法,那么线程的概念就是在库中维护的,在库中就一定存在多个被创建好的线程,库需要管理这些线程!先描述,在组织!

pthread_create();
|
|
struct tcb
{// 线程的属性// 线程的状态  joinoable// 线程id// 线程的独立栈结构// 线程栈的大小// ...
};

我们每创建一个线程,库内部会创建一个管理的部分tcb。

  • TID: 并不是LWP,而是线程库中当前线程管理模块的起始地址!struct_pthread中存在一个void* ret用于接收调用函数的返回值,因此我们需要使用pthread_join去获取ret并且释放这块管理模块的空间!pthread_join通过传入的参数tid获取该管理模块的地址!线程调用结束底层内核的LWP会被释放,但是动态库的管理模块还存在着!

  • 线程返回值:线程执行完将返回值写入到线程的控制管理模块,主线程使用pthread_join进行获取

  • 线程分离:在线程控制块中的线程状态设置一下!

  • Linux的所有线程都在线程库中。

  • 线程局部存储的使用:我们前面使用到的__pthread int a = 0;

  • 用户线程 Vs LWP 如何联动

    • 每个线程都有自己独立的栈空间,栈空间存在于动态库的管理模块内部!

    • pthread_create

      • 在库中创建对应线程的管理模块

      • 在内核中创建对应的轻量级线程,调用系统调用clone(),将对应的栈和调用的函数传入,线程在CPU调用就可以执行自己的代码和使用自己的数据了!

       

      • Linux用户级线程 :内核级线程 = 1 :1

      7. 线程封装

      基于我们上面所了解的内容,我们是否也可以实现一个和C++11类似的线程对象呢,我们想要实现的效果如下:

      #ifndef __MYPTHREAD__HPP
      #define __MYPTHREAD__HPP#include <iostream>
      #include <pthread.h>
      #include <string>
      #include <functional>
      #include <cstring>
      #include <unistd.h>
      #include <vector>namespace ThreadSpace
      {/* 用于标识线程的name */static int num = 1;/* 用于标识当前线程的状态 */enum class STATUS{NEW,    /* 新创建 */RUNING, /* 运行中 */STOP    /* 暂停中 */    };/* 包装器:包装回调函数 */using func_t = std::function<void()>;/* Thread类 */class Thread{/* 设置分离状态 */void EnableJoin(){_is_detachable = true;}public:Thread(func_t routine, bool is_detach = false) /* 缺省参数:回调函数, 初始是否分离状态 */: _tid(0), _pid(getpid()), _name("thread-" + num++), _is_detachable(is_detach) /* 默认不分离 */, _routine(routine), _ret(nullptr), _status(STATUS::NEW){}/* 函数回调接口 */static void *Routine(void *args) /* 必须使用静态成员函数,否则带有this指针 */{Thread *t = static_cast<Thread *>(args);t->_routine(); /* 回调函数 *//* 自然结束更新状态 */t->_status = STATUS::STOP;/* 返回值 */return nullptr;}/* 创建线程 */bool Start(){/* 如果线程已经处于运行状态不要重新运行 */if(_status == STATUS::RUNING){/* 属于重复运行情况 */std::cout << "Runing again!" << std::endl;return false;}/* 创建一个线程 */int n = pthread_create(&_tid, nullptr, Routine, static_cast<void *>(this));if (n != 0){std::cout << "pthread_create fail!" << strerror(n) << std::endl;return false;}/* 更改状态为Runing */_status = STATUS::RUNING;std::cout << "pthread_create success!" << std::endl;/* 查看是否初始为分离状态,如果是进行分离 */if(_is_detachable == true)pthread_detach(_tid);return true; }/* 分离线程 */bool Detach(){if(_is_detachable == true){/* 如果线程已经分离,不需要重新分离 */std::cout << "Detach again!" << std::endl;return false;}/* 进行分离 *//* 1. 分离 */int n = pthread_detach(_tid);if(n != 0){std::cout << "pthread_detach fail" << strerror(n) << std::endl;return false;}/* 2. 设置分离状态 */EnableJoin();std::cout << "pthread_detach success!" << std::endl;return true;}bool Join(){/* 如果线程已经分离,就不要join */if (_is_detachable == true){std::cout << "pthread is detach, not need join!" << std::endl;return false;}int n = pthread_join(_tid, &_ret);if (n != 0){std::cout << "pthread_join fail!" << strerror(n) << std::endl;return false;}/* 调整进程状态 */_status = STATUS::STOP;std::cout << "pthread_join success!" << std::endl;return true;}bool Cancel(){/* 取消的进程状态必须属于执行中的进程 */if(_status != STATUS::RUNING){std::cout << "you cancel thread is not runing!" << std::endl;return false;}int n = pthread_cancel(_tid);if (n != 0){std::cout << "pthread_cancel fail" << strerror(n) << std::endl;return false;}/* 调整进程状态 */_status = STATUS::STOP;std::cout << "pthread_cancel success!" << std::endl;return true;}void* GetRet(){return _ret;}~Thread(){}private:pthread_t _tid;    /* 线程tid */pid_t _pid;        /* 进程pid */std::string _name; /* 线程name */bool _is_detachable;  /* 线程是否分离 */func_t _routine;   /* 线程执行的函数 */void *_ret;        /* 用于获取返回值 */STATUS _status;    /* 线程状态 */};
      }namespace ThreadSpace_T
      {/* 用于标识线程的name */static int num = 1;/* 用于标识当前线程的状态 */enum class STATUS{NEW,    /* 新创建 */RUNING, /* 运行中 */STOP    /* 暂停中 */    };/* Thread类 */template<class T>class Thread{/* 包装器:包装回调函数 */using func_t = std::function<void(T)>;/* 设置分离状态 */void EnableJoin(){_is_detachable = true;}public:Thread(func_t routine, T data, bool is_detach = false) /* 缺省参数:回调函数, 初始是否分离状态 */: _tid(0), _pid(getpid()), _name("thread-" + num++), _is_detachable(is_detach) /* 默认不分离 */, _routine(routine), _ret(nullptr), _status(STATUS::NEW), _data(data){}/* 函数回调接口 */static void *Routine(void *args) /* 必须使用静态成员函数,否则带有this指针 */{Thread<T> *t = static_cast<Thread<T> *>(args);t->_routine(t->_data); /* 回调函数 *//* 自然结束更新状态 */t->_status = STATUS::STOP;/* 返回值 */return nullptr;}/* 创建线程 */bool Start(){/* 如果线程已经处于运行状态不要重新运行 */if(_status == STATUS::RUNING){/* 属于重复运行情况 */std::cout << "Runing again!" << std::endl;return false;}/* 创建一个线程 */int n = pthread_create(&_tid, nullptr, Routine, this);if (n != 0){std::cout << "pthread_create fail!" << strerror(n) << std::endl;return false;}/* 更改状态为Runing */_status = STATUS::RUNING;std::cout << "pthread_create success!" << std::endl;/* 查看是否初始为分离状态,如果是进行分离 */if(_is_detachable == true)pthread_detach(_tid);return true; }/* 分离线程 */bool Detach(){if(_is_detachable == true){/* 如果线程已经分离,不需要重新分离 */std::cout << "Detach again!" << std::endl;return false;}/* 进行分离 *//* 1. 分离 */int n = pthread_detach(_tid);if(n != 0){std::cout << "pthread_detach fail" << strerror(n) << std::endl;return false;}/* 2. 设置分离状态 */EnableJoin();std::cout << "pthread_detach success!" << std::endl;return true;}bool Join(){/* 如果线程已经分离,就不要join */if (_is_detachable == true){std::cout << "pthread is detach, not need join!" << std::endl;return false;}int n = pthread_join(_tid, &_ret);if (n != 0){std::cout << "pthread_join fail!" << strerror(n) << std::endl;return false;}/* 调整进程状态 */_status = STATUS::STOP;std::cout << "pthread_join success!" << std::endl;return true;}bool Cancel(){/* 取消的进程状态必须属于执行中的进程 */if(_status != STATUS::RUNING){std::cout << "you cancel thread is not runing!" << std::endl;return false;}int n = pthread_cancel(_tid);if (n != 0){std::cout << "pthread_cancel fail" << strerror(n) << std::endl;return false;}/* 调整进程状态 */_status = STATUS::STOP;std::cout << "pthread_cancel success!" << std::endl;return true;}void* GetRet(){return _ret;}~Thread(){}private:pthread_t _tid;    /* 线程tid */pid_t _pid;        /* 进程pid */std::string _name; /* 线程name */bool _is_detachable;  /* 线程是否分离 */func_t _routine;   /* 线程执行的函数 */void *_ret;        /* 用于获取返回值 */STATUS _status;    /* 线程状态 */T _data;           /* 对象类型 */};
      }#endif

      相关文章:

      【Linux】线程的概念与控制

      目录 1. 整体学习思维导图 2. 线程的概念 2.1 基础概念 2.2 Linux下的线程 初步理解&#xff1a; 2. 分页式存储 3.1 页表 3.1.1 页框/页 3.1.2 页表机制 3.1.3 从虚拟地址到物理地址的转换 总结&#xff1a; 3.2 TLB快表 3.3 缺页异常&#xff08;Page Fault&am…...

      K8s 老鸟的配置管理避雷手册

      Yining, China 引言 对于这种案例&#xff0c;你们的处理思路是怎么样的呢&#xff0c;是否真正的处理过&#xff0c;如果遇到&#xff0c;你们应该怎么处理。 最后有相关的学习群&#xff0c;有兴趣可以加入。 开始 一、血泪教训&#xff1a;环境变量引发的真实灾难 1.1 …...

      飞速(FS)解决方案验证实验室搬迁升级,赋能客户技术服务

      飞速&#xff08;FS&#xff09;解决方案验证实验室近日顺利完成搬迁升级&#xff0c;标志着飞速&#xff08;FS&#xff09;在解决方案可行性验证、质量保障以及定制化需求支持方面迈上新台阶&#xff0c;进一步提升了产品竞争力和客户信任度。 全新升级的实验室定位为技术验证…...

      柔性关节双臂机器人奇异摄动鲁棒自适应PD控制

      1 双臂机器人动力学模型 对于一个具有多个关节的机器人来说&#xff0c;机器人端动力学子方程及关节驱动电机端动力学子方程为&#xff1a; 以上为推导过程&#xff0c;MATLAB程序已完成&#xff0c;若需要可找我。...

      遵循IEC62304YY/T0664:确保医疗器械软件生命周期合规性

      一、EC 62304与YY/T 0664的核心定位与关系 IEC 62304&#xff08;IEC 62304&#xff09;是国际通用的医疗器械软件生命周期管理标准&#xff0c;适用于所有包含软件的医疗器械&#xff08;如嵌入式软件、独立软件、移动应用等&#xff09;&#xff0c;其核心目标是确保软件的安…...

      Kafka和RocketMQ相比有什么区别?那个更好用?

      Kafka和RocketMQ相比有什么区别?那个更好用? Kafka 和 RocketMQ 都是广泛使用的消息队列系统&#xff0c;它们有很多相似之处&#xff0c;但也有一些关键的区别。具体选择哪个更好用&#xff0c;要根据你的应用场景和需求来决定。以下是它们之间的主要区别&#xff1a; 1. …...

      空对象模式(Null Object Pattern)在C#中的实现详解

      一 、什么是空对象模式 空对象模模是靠”空对孔象式是书丯一种引施丼文行为,行凌,凌万成,个默疤"空象象象象来飞䛿引用用用用电从延盈盈甘仙丿引用用用职从延务在仅代砷易行行 」这种燕式亲如要目的片片 也说媚平父如如 核心思烟 定义一个人 派一个 &#xfffd; 创建…...

      【Windows】Win2008服务器SQL服务监控重启脚本

      以下是一个用于监控并自动重启 SQL Server 服务的批处理脚本&#xff0c;适用于 Windows Server 2008 和 SQL Server 2012&#xff08;默认实例&#xff09;&#xff1a; echo off setlocal enabledelayedexpansion:: 配置参数 set SERVICE_NAMEMSSQLSERVER set LOG_FILEC:\SQ…...

      Spring MVC 操作会话属性详解(@SessionAttributes 与 @SessionAttribute)

      Spring MVC 操作会话属性详解&#xff08;SessionAttributes 与 SessionAttribute&#xff09; 1. 核心注解对比 注解作用范围功能SessionAttributes类级别声明控制器中需要持久化的模型属性&#xff08;存入 HttpSession&#xff09;SessionAttribute方法参数/返回值显式绑定…...

      416. 分割等和子集

      416. 分割等和子集 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割成 [1, 5, 5] 和…...

      Composer安装Laravel步骤

      Composer安装Laravel步骤 要使用 Composer 安装 Laravel&#xff0c;请按照以下步骤操作&#xff1a; 确保已经安装了 Composer。如果还没有安装&#xff0c;请访问 https://getcomposer.org/download/ 下载并安装。 打开命令行或终端。 使用 cd 命令导航到你的项目目录&…...

      游戏引擎学习第209天

      调整椅子α 昨天&#xff0c;我们实现了将数据输出到调试流中的功能&#xff0c;之前的调试流大多只包含性能分析数据&#xff0c;而现在我们可以将任意数据放入调试流中。 完成这个功能后&#xff0c;我们接下来要做的是收集这些数据并显示出来&#xff0c;这样我们就能有一…...

      更新vscode后链接远程服务器出现了报错‘无法建立连接:远程主机不满足运行vscode服务器的先决条件’20250408

      更新了vscode之后再链接远程服务器出现了报错&#xff0c;如下&#xff1a; 1. 确认服务器上的库版本 1.1 检查 glibc 版本 在服务器终端运行&#xff1a; ldd --version 最低要求&#xff1a;VS Code 远程开发需要 glibc ≥ 2.28。 1.2 检查 libstdc 版本 在服务器终端运…...

      电磁兼容特种测试

      并非所有的检测都能在实验室的标准场地中完成。今天&#xff0c;就带大家走进电磁兼容特种测试中需要现场测试的情况&#xff0c;看看哪些场合和设备有着特殊的测试需求。 哪种场合需要现场测试&#xff1f; 大型设备由于物理尺寸或供电功率上的限制&#xff0c;无法在一般…...

      PyTorch 基础要点详解:从模型构建到评估

      在深度学习领域&#xff0c;PyTorch 作为一款广受欢迎的开源框架&#xff0c;为开发者提供了便捷高效的工具。今天&#xff0c;我们就深入探讨一下 PyTorch 中的几个关键要点&#xff1a;torch.nn.Linear、torch.nn.MSELoss、model.train() 以及 model.eval()&#xff0c;了解它…...

      Dockerfile中CMD命令未生效

      今天在使用dockerfile构建容器镜像时&#xff0c;最后一步用到CMD命令启动start.sh&#xff0c;但是尝试几遍都未能成功执行脚本。最后查阅得知&#xff1a;Dockerfile中可以有多个cmd指令&#xff0c;但只有最后一个生效&#xff0c;CMD会被docker run之后的参数替换。 CMD会…...

      Linux平台MQTT测试抓包分析

      Linux平台搭建MQTT测试环境-CSDN博客基于这里的测试代码抓包 sudo tcpdump -i any -w mqtt1.cap 上述源码中 tcp://localhost:1883 配置连接&#xff1a; Broker Address: localhostPort: 1883 整体通信流程 1. Subscriber和Broker&#xff08;代理服务器&#xff09;建立…...

      Docker全方位指南

      目录 前言 第一部分&#xff1a;Docker基础与安装 1.1 什么是Docker&#xff1f; 1.2 Docker的适用场景 1.3 全平台安装指南 1.4 配置优化 第二部分&#xff1a;Docker核心操作与原理 2.1 镜像管理 2.2 容器生命周期 2.3 网络模型 2.4 Docker Compose 第三部分&…...

      【经典DP】三步问题 / 整数拆分 / 不同路径II / 过河卒 / 下降路径最小和 / 地下城游戏

      ⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;动态规划 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 动态规划总结Fibonacci数列BC140 杨辉三角杨辉三角三步问题最小花费爬楼梯孩子们的游戏解码方法整数拆分不同路径不同路径II过…...

      Koji/OBS编译节点OS版本及工具版本管理深度实践指南

      引言 在分布式编译框架Koji/OBS中&#xff0c;有效管理编译节点的操作系统&#xff08;OS&#xff09;版本及工具版本是确保构建环境稳定性、兼容性和安全性的关键。本文将从多版本共存、自动化更新、兼容性管理等多个维度&#xff0c;系统阐述如何高效管理编译节点的OS版本及…...

      39、web前端开发之Vue3保姆教程(三)

      四、Vue3中集成Element Plus 1、什么是Element Plus Element Plus 是一款基于 Vue 3 的开源 UI 组件库,旨在为开发者提供一套高质量、易用的组件,用于快速构建现代化的 web 应用程序。 Element Plus 提供了大量的 UI 组件,包括但不限于: 表单组件:输入框、选择器、开关…...

      多类型医疗自助终端智能化升级路径(代码版.下)

      医疗人机交互层技术实施方案 一、多模态交互体系 1. 医疗语音识别引擎 # 基于Wav2Vec2的医疗ASR系统 from transformers import Wav2Vec2Processor, Wav2Vec2ForCTC import torchaudioclass MedicalASR:def __init__(self):self.processor = Wav2Vec2Processor.from_pretrai…...

      Git代码管理

      这里写目录标题 分支管理策略TrunkBased&#x1f331; 核心理念✅优点❌缺点适用场景 GitFlow✅ GitFlow 的优点❌ GitFlow 的缺点适用场景 AOneFlow✅ AOneFlow 的优点❌缺点适用场景 如何选择分支策略&#xff1f;代码提交规范&#x1f331;分支管理&#x1f504;代码更新⚔️…...

      CubeMX配置STM32F103PWM连续频率输出

      要求&#xff1a; 输出2-573Hz频率&#xff0c;输出频率步长小于1Hz 一、CubeMX配置 auto-reload preload在下个周期加载ARR Output compare preload 在下个周期加载CCR 二、 程序 1.启动PWM输出 HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1); 2.根据频率调整PSC、ARR、…...

      举例说明计算机视觉(CV)技术的优势和挑战。

      计算机视觉(CV)技术是人工智能领域的一个重要分支,通过让计算机“看”和“理解”图像或视频,可以实现许多实际应用。以下是计算机视觉技术的优势和挑战的例子: 优势: 自动化处理:CV技术可以自动化地处理大量图像或视频数据,实现快速而准确的分析和识别。提高效率:在许…...

      工程师 - FTDI SPI converter

      中国网站&#xff1a;FTDIChip- 首页 UMFT4222EV-D UMFT4222EV-D - FTDI 可以下载Datasheet。 UMFT4222EVUSB2.0 to QuadSPI/I2C Bridge Development Module Future Technology Devices International Ltd. The UMFT4222EV is a development module which uses FTDI’s FT4222H…...

      河畔石上数(C++)

      在 C 里&#xff0c;std::set 是标准模板库&#xff08;STL&#xff09;提供的一种关联容器&#xff0c;它能高效地存储唯一元素&#xff0c;并且元素会按照特定的顺序排列&#xff0c;默认是升序。下面从多个方面为你详细介绍 std::set。 1. 头文件包含 若要使用 std::set&a…...

      《线性表、顺序表与链表》教案(C语言版本)

      &#x1f31f; 各位看官好&#xff0c;我是maomi_9526&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习C语言的相关知识。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c;分享给更…...

      【用Cursor 进行Coding 】

      「我」&#xff1a;“添加 XXX 功能” &#xff3b;Claude-3.7&#xff3d;:“好的&#xff0c;我完成了&#xff0c;还顺手做了 19个你没要求不需要的功能、甚至还修改了原有999行正常代码 &#xff5e; 不用谢” &#xff3b;Gemini-2.5&#xff3d;:“好的&#xff0c;我会…...

      vue2 打包时增加时间戳防止浏览器缓存,打包后文件进行 js、css 压缩

      文章目录 前言一、什么是浏览器缓存二、展示效果三、vue.config.js 代码四、代码压缩部分服务器不支持五、感谢 前言 vue 开发过程中&#xff0c;项目前端代码需要更新&#xff0c;更新后由于浏览器缓存导致代码没有及时更新所产生错误&#xff0c;所以在打包时增加时间戳防止…...

      TIM定时器

      一、TIM定时器 STM32高级定时器实战&#xff1a;PWM、捕获与死区控制详解-CSDN博客 二、相关函数 1.TIM_TimeBaseInitTypeDef结构体讲解 typedef struct {uint16_t TIM_Prescaler; // 预分频器&#xff0c;用于设置定时器计数频率uint16_t TIM_CounterMode; /…...

      S130N-ISI 全栈方案与云平台深度协同:重构 PLC 开发新范式

      一、什么是 PLC&#xff1f; 1.技术定义 PLC&#xff08;Power Line Communication&#xff09;是一种创新的通信技术&#xff0c;它以电力线作为天然的传输介质&#xff0c;通过先进的信号调制技术将高频数据信号叠加于工频电流之上&#xff0c;实现电力输送与数据通信的双频共…...

      Jenkins 插件文件优先使用 .jpi 后缀

      .hpi 和 .jpi 文件本质上是 Jenkins 插件的打包格式&#xff0c;两者的区别主要体现在历史和命名习惯上&#xff1a; ✅ .hpi&#xff08;Hudson Plugin&#xff09; 来源&#xff1a;最初是 Hudson 项目的插件格式。含义&#xff1a;Hudson Plugin 的缩写。用途&#xff1a;早…...

      # 决策树与PCA降维在电信客户流失预测中的应用

      决策树与PCA降维在电信客户流失预测中的应用 在数据分析和机器学习领域&#xff0c;电信客户流失预测是一个经典的案例。本文将通过Python代码实现&#xff0c;探讨决策树模型在电信客户流失预测中的应用&#xff0c;并结合PCA降维技术优化模型性能&#xff0c;同时对比降维前…...

      go语言的语法糖以及和Java的区别

      1. Go 语言的语法糖及简化语法 Go 语言本身设计理念是简洁、清晰&#xff0c;虽然不像某些动态语言那样“花哨”&#xff0c;但它提供了几种便捷语法&#xff0c;使代码更简洁&#xff1a; 1.1 短变量声明&#xff08;Short Variable Declaration&#xff09; 语法&#xff1…...

      WebRtc 视频流卡顿黑屏解决方案

      // node webrtc视频转码服务 const url "http://10.169.xx.xx:8000" <video :ref"videoRefs${index}" :id"videoRefs4_${index}" :src"item" controls:key"item" autoplay muted click"preventDefaultClick"…...

      信息安全测评中心-国产化!

      项目上使用产品&#xff0c;必须通过国家信息安全测评/ 信息技术产品安全测评&#xff0c;有这个需求的话&#xff0c;可以到CN信息安全测评中心官网中的--测评公告一栏中&#xff0c;找符合要求的产品。 测评公告展示的包括硬件产品、系统、服务资质等。 网址及路径&#xf…...

      MySQL学习笔记九

      第十一章使用数据处理函数 11.1函数 SQL支持函数来处理数据但是函数的可移植性没有SQL强。 11.2使用函数 11.2.1文本处理函数 输入&#xff1a; SELECT vend_name,UPPER(vend_name) AS vend_name_upcase FROM vendors ORDER BY vend_name; 输出&#xff1a; 说明&#…...

      DFS 蓝桥杯

      最大数字 问题描述 给定一个正整数 NN 。你可以对 NN 的任意一位数字执行任意次以下 2 种操 作&#xff1a; 将该位数字加 1 。如果该位数字已经是 9 , 加 1 之后变成 0 。 将该位数字减 1 。如果该位数字已经是 0 , 减 1 之后变成 9 。 你现在总共可以执行 1 号操作不超过 A…...

      动态规划dp专题-(上)

      目录 dp理论知识&#x1f525;&#x1f525; &#x1f3af;一、线性DP &#xff08;1&#xff09;&#x1f680;斐波那契数 -入门级 &#xff08;2&#xff09;&#x1f680;898. 数字三角形-acwing ---入门级 &#xff08;3&#xff09;往期题目 ①选数异或&#xff1a;在…...

      正则表达式(一)

      一、模式&#xff08;Patterns&#xff09;和修饰符&#xff08;flags&#xff09; 通过正则表达式&#xff0c;我们可以在文本中进行搜索和替换操作&#xff0c;也可以和字符串方法结合使用。 正则表达式 正则表达式&#xff08;可叫作 “regexp”&#xff0c;或 “reg”&…...

      需求变更导致成本超支,如何止损

      需求变更导致成本超支时&#xff0c;可以通过加强需求管理、严格的变更控制流程、优化资源配置、实施敏捷开发、提高风险管理意识等方法有效止损。其中&#xff0c;加强需求管理是止损的核心措施之一。需求管理涉及需求明确化、需求跟踪和变更的管理&#xff0c;有效的需求管理…...

      《数据分析与可视化》(清华)ch5-实训代码

      小费数据集预处理——求思考题_有问必答-CSDN问答 以上代码在Jupyter Notebook中可以运行&#xff0c;但是在python中就会出如下问题&#xff1a; 这个错误表明在尝试计算均值填充缺失值时&#xff0c;数据中包含非数值类型的列&#xff08;如文本列&#xff09;&#xff0c;…...

      E: The package APP needs to be reinstalled, but I can‘t find an archive for it.

      要解决错误 “E: The package mytest needs to be reinstalled, but I can’t find an archive for it”&#xff0c;通常是因为系统中存在损坏的软件包记录或安装过程中断导致 /var/lib/dpkg/status 文件异常。以下是综合多篇搜索结果的解决方案&#xff1a; 解决步骤 备份关…...

      若依startPage()详解

      背景 startPage基于PageHelper来进行强化&#xff0c;在用户传入pagesize,pageNum等标准参数的时候不需要进行解析 步骤 1.通过ServletUtils工具类getRequestAttributes来获取当前线程的上下文信息 public static ServletRequestAttributes getRequestAttributes() {try {R…...

      Oracle AQ

      Oracle AQ&#xff08;Advanced Queuing&#xff09; 是 Oracle 数据库内置的一种消息队列&#xff08;Message Queue&#xff09;技术&#xff0c;用于在应用或系统之间实现异步通信、可靠的消息传递和事件驱动架构。它是 Oracle 数据库的核心功能之一&#xff0c;无需依赖外部…...

      npm报错CERT_HAS_EXPIRED解决方案

      npm报错解决方案 npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED方案1:尝试切换镜像 # 使用腾讯云镜像 npm config set registry https://mirrors.cloud.tencent.com/npm/# 或使用官方npm源&#xff08;科学上网&#xff09; npm config set registry http…...

      pnpm 中 Next.js 模块无法找到问题解决

      问题概述 项目在使用 pnpm 管理依赖时,出现了 “Cannot find module ‘next/link’ or its corresponding type declarations” 的错误。这是因为 pnpm 的软链接机制在某些情况下可能导致模块路径解析问题。 问题诊断 通过命令 pnpm list next 确认项目已安装 Next.js 15.2.…...

      急速实现Anaconda/Miniforge虚拟环境的克隆和迁移

      目录 参考资料 点击Anaconda Prompt (anaconda_base) 查看现有环境 开始克隆&#xff0c;以克隆pandas_env为例&#xff0c;新的环境名字为image (base) C:\Users\hello>conda create -n image --clone pandas_env查看克隆结果&#xff0c;image环境赫然在列。 然后粘贴…...

      OpenCv高阶(二)——图像的掩膜

      目录 掩膜 bitwise_and原理 掩膜的实现 1、基于像素操作 2、使用形态学操作 3、基于阈值处理 案例 1、读取原图并绘制掩膜 2、掩膜的实现 3、绘制掩膜的直方图 应用 掩膜 OpenCV 中图像掩膜&#xff08;Mask&#xff09;实现的原理是通过一个与原始图像大小相同的二…...