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

【Linux系统编程】第四十二弹---多线程编程全攻略:涵盖线程创建、异常处理、用途、进程对比及线程控制

 ✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、线程创建

2、线程异常

3、线程用途

4、进程 VS 线程

5、线程控制

5.1、创建和等待线程


1、线程创建

线程能看到进程的大部分资源,下面做一个对全局变量修改的测试验证!!!

代码演示 

int gval = 100;void* threadStart(void* args)
{// 新线程while(true){std::cout << "new thread running..." << ",pid: " << getpid()<< ",gval: " << gval << ",&gval: " << &gval << std::endl;sleep(1);}
}// 线程访问全局变量
int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadStart,(void*)"thread-new");// 主线程while(true){std::cout << "main thread running..." << ",pid: " << getpid()<< ",gval: " << gval << ",&gval: " << &gval << std::endl;gval++; // 主线程修改全局变量sleep(1);}return 0;
}

运行结果  

2、线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

代码演示  

// 单个线程崩溃,会导致进程崩溃
int gval = 100;void *threadStart(void *args)
{// 新线程while (true){sleep(1);int x = rand() % 5; // 生成0-4的随机数std::cout << "new thread running..." << ",pid: " << getpid()<< ",gval: " << gval << ",&gval: " << &gval << std::endl;// 随机数等于0则让线程崩溃if (x == 0){int *p = nullptr; // 空指针解引用问题*p = 100;}}
}// 线程访问全局变量
int main()
{srand(time(nullptr));// 创建3个线程pthread_t tid1;pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new1");pthread_t tid2;pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new2");pthread_t tid3;pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new3");// 主线程while (true){std::cout << "main thread running..." << ",pid: " << getpid()<< ",gval: " << gval << ",&gval: " << &gval << std::endl;gval++; // 主线程修改全局变量sleep(1);}return 0;
}

运行结果 

3、线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

4、进程 VS 线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器(保存硬件上下文数据)
    • 栈(程序在运行的时候,会形成各种临时变量,临时变量被每个线程保存在自己的栈区)
    • errno
    • 信号屏蔽字
    • 调度优先级


进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

如何看待之前学习的单进程?

具有一个线程执行流的进程 。

线程调度成本为什么比进程更低?(面试题)

一、进程与线程的基本概念

  1. 进程:进程是资源分配的最小单位,每个进程都有自己独立的地址空间,系统需要为进程分配地址空间并建立数据表来维护其代码段、堆栈段和数据段。这种操作相对复杂且开销较大。
  2. 线程:线程是程序执行的最小单位(资源调度的最小单位),它是进程的一部分,共享进程所拥有的资源。因此,线程切换时无需像进程切换那样重新分配地址空间和维护数据表,从而减少了开销。

二、上下文切换的开销

  1. CPU上下文切换:无论是进程调度还是线程调度,都需要进行CPU上下文切换。这部分开销在两者中是相似的。
  2. CPU Cache/TLB命中率线程切换时,由于多个线程共享进程的地址空间,因此CPU Cache(高速缓存)和TLB(转换后备缓冲器)中的内容在切换后仍然有效,命中率较高。而进程切换时,由于地址空间的变化,原有的Cache和TLB内容可能不再适用,导致命中率下降,触发更多的缺页中断,从而增加了开销。

三、资源共享与通信

  1. 资源共享:线程共享进程的资源,包括地址空间、全局变量、静态变量等。这使得线程之间的通信更加便捷,无需像进程间通信那样通过IPC(进程间通信)方式进行,从而减少了通信开销。
  2. 通信开销进程间通信需要借助额外的机制(如管道、信号、共享内存等),这些机制的实现和维护都会增加开销。而线程间通信则可以直接通过共享内存进行,无需额外的通信机制。

四、创建与销毁的开销

  1. 创建开销:由于进程需要分配独立的地址空间和维护数据表,因此创建进程的开销相对较大。而线程则共享进程的地址空间,无需进行这些操作,因此创建线程的开销较小。
  2. 销毁开销:同样地,由于进程拥有独立的资源,因此在销毁时需要释放这些资源,开销较大。而线程则无需释放独立的资源,销毁开销相对较小。

5、线程控制

线程控制:创建,终止,等待,分离

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

5.1、创建和等待线程

pthread_join()

pthread_join - 等待指定的线程终止#include <pthread.h>int pthread_join(pthread_t thread, void **retval);

参数:

  • pthread_t thread:这是你想要等待的线程的标识符。线程标识符是在创建线程时通过 pthread_create 函数返回的。
  • void **retval:这是一个指向指针的指针(二级指针),用于接收被等待线程的返回值。如果你不需要获取线程的返回值,可以将这个参数设置为 nullptr。被等待线程的返回值应该是一个 void* 类型的指针,在调用 pthread_exit 或从线程的启动函数返回时设置。

返回值:

  • 成功时pthread_join 返回 0
  • 失败时,返回一个错误码。常见的错误码包括:
    • ESRCH:指定的线程不存在。
    • EINVAL:线程不是可连接的(即,线程不是可加入的,可能因为它已经终止了,或者它是以分离状态创建的)。
    • EDEADLK:检测到死锁(在尝试加入一个已经由调用线程加入的线程时可能发生)。
    • 其他可能的错误码,具体取决于系统实现。

代码演示

新线程执行函数

void *threadRun(void *args)
{int cnt = 10;while(cnt){// 每隔一秒打印一次std::cout << "new thread run...,cnt: " << cnt-- << std::endl;sleep(1);}return nullptr;
}

主函数

int main()
{pthread_t tid;// 创建新线程int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");if (n != 0) // 后面暂时不关心{std::cerr << "create thread errno " << std::endl;return 1;}std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果 

问题1 : main 和 new 线程谁先运行?

不确定

问题2 : 我们期望谁最后退出?

main thread最后退出,类似与父进程最后退出,回收子进程 , 你如何保证呢?

  • join来保证不join呢?
    • 主线程活着,新线程退出会造成类似僵尸问题

问题3 :tid是什么样子的?是什么呢?

tid通过10进制打印是一个很大的值,tid实际上是一个虚拟地址,可以通过16进制进行打印。

打印函数

// 10进制打印tid
void PrintToDec(pthread_t &tid)
{std::cout << "tid: " << tid << std::endl; 
}// 16进制打印tid
std::string PrintToHex(pthread_t &tid)
{char buffer[128];snprintf(buffer,sizeof(buffer),"0x%lx",tid);return buffer;
}

主函数 

int main()
{pthread_t tid;// 创建新线程int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");// 问题3 : tid是什么样子的?是什么呢?虚拟地址! 为什么?PrintToDec(tid); // 按照10进制方式打印std::string tid_str = PrintToHex(tid); // 按照16进制方式打印std::cout << "tid: " << tid_str << std::endl;std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果 

问题4 : 全面看待线程函数传参?

我们可以传递任意类型,但你一定要能想得起来,也能传递类对象地址!! 

方式一:传字符串常量

代码演示 

void *threadRun(void *args)
{std::string name = (const char*)args;int cnt = 10;while(cnt){// 每隔一秒打印一次std::cout << name << " run...,cnt: " << cnt-- << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;// 创建新线程int n = pthread_create(&tid, nullptr, threadRun, (void *)"thread 1");std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来std::cout << "tid: " << tid_str << std::endl;std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果 

方式二:传整数

代码演示 

void *threadRun(void *args)
{int a = *(int*)args;// warning 系统为64位,指针大小为8字节,int为4字节int cnt = 10;while(cnt){std::cout << a << " run...,cnt: " << cnt-- << std::endl;sleep(1);}return nullptr;
}
int main()
{pthread_t tid;int a = 100;int n = pthread_create(&tid, nullptr, threadRun, (void *)&a);std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来std::cout << "tid: " << tid_str << std::endl;std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果 

 方式二:传类对象

代码演示 

class ThreadData
{
public:std::string name;int num;
};
void *threadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)argsint cnt = 10;while(cnt){std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl; sleep(1);}return nullptr;
}

主函数

int main()
{pthread_t tid;ThreadData td;td.name = "thread-1";td.num = 1;int n = pthread_create(&tid, nullptr, threadRun, (void*)&td); // 传递线程结构体对象std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来std::cout << "tid: " << tid_str << std::endl;std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果  

创建新线程访问栈上的空间不推荐,因为当多个新线程访问同一个结构体数据时,可能造成数据互相影响的问题,如果只读问题不大,但是如果一个线程对该数据进行修改,那么后面所有线程访问的数据都会修改!!!

// 再创建一个新线程,使用同一个局部变量,修改值两个都修改了
td.name = "thread-2";
td.num = 2;
n = pthread_create(&tid, nullptr, threadRun, (void*)&td); // 传递线程结构体对象

运行结果 

推荐在堆上申请空间,一个新线程申请一个类对象,使用完毕释放空间! 

void *threadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)argsint cnt = 10;while(cnt){std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl; sleep(1);}std::cout << "delete td:" << td << std::endl;delete td; // 释放空间return nullptr;
}
int main()
{pthread_t tid;ThreadData* td = new ThreadData();td->name = "thread-1";td->num = 1;int n = pthread_create(&tid, nullptr, threadRun, td); std::string tid_str = PrintToHex(tid); // 按照16进制方式打印出来std::cout << "tid: " << tid_str << std::endl;std::cout << "main thread join begin..." << std::endl;// 等待新线程终止n = pthread_join(tid,nullptr); if(n == 0){std::cout << "main thread wait success " << std::endl;}return 0;
}

运行结果 

问题5: 全面看待线程函数返回:? 

新线程函数返回值

1、只考虑正确的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程。

新线程通过函数返回值给主线程!!!

代码演示  

void *threadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args); // 安全类别强转 (ThreadData*)argsint cnt = 10;while(cnt){std::cout << td->name << " run...,num is " << td->num << ",cnt: " << cnt-- << std::endl; // int* p = nullptr;// *p = 100; // 故意野指针sleep(1);}std::cout << "delete td:" << td << std::endl;delete td; // 释放空间return (void*)111;
}

主线程获取新线程的返回值信息!!! 

int main()
{pthread_t tid;ThreadData* td = new ThreadData();td->name = "thread-1";td->num = 1;int n = pthread_create(&tid, nullptr, threadRun, td); std::cout << "main thread join begin..." << std::endl;// 等待新线程终止void* code = nullptr; // 开辟了空间的!!!n = pthread_join(tid,&code); if(n == 0){// 主线程拿新线程的退出信息,int会有精度损失,Linux中地址8字节,int4字节std::cout << "main thread wait success, new thread exit code: " << (uint64_t)code << std::endl;}return 0;
}

运行结果 

 新线程故意野指针!!!

运行结果  

2、我们可以传递任意类型,但你一定要能想得起来,也能传递类对象地址!! 

类对象

class ThreadData
{
public:int Excute(){return x + y;}
public:std::string name;int x;int y;
};class ThreadResult
{
public:std::string Print(){return std::to_string(x) + "+" + std::to_string(y) + "=" + std::to_string(result);}
public:int x;int y;int result;
};

新线程函数

void *threadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args); int cnt = 10;ThreadResult* result = new ThreadResult();while(cnt){sleep(3);std::cout << td->name << " run...,cnt: " << cnt-- << std::endl; result->result = td->Excute();result->x = td->x;result->y = td->y;break;}std::cout << "delete td:" << td << std::endl;delete td; // 释放空间return (void*)result;
}

主函数

int main()
{pthread_t tid;ThreadData* td = new ThreadData();td->name = "thread-1";td->x = 10;td->y = 20;int n = pthread_create(&tid, nullptr, threadRun, td); std::cout << "main thread join begin..." << std::endl;// 等待新线程终止ThreadResult* result = nullptr; // 开辟了空间的!!!n = pthread_join(tid,(void**)&result); if(n == 0){std::cout << "main thread wait success, new thread exit code: " << result->Print() << std::endl;}return 0;
}

运行结果 

问题6 : 如何创建多线程呢? 

错误示范(X)

在for循环内部创建临时变量!!!

代码演示  

const int num = 10;void *threadrun(void *args)
{std::string name = static_cast<const char *>(args);while (true){// 打印的线程名是乱的,线程执行顺序是不确定的,// 且因为在名字栈区for循环内部创建,每切换一个线程,名字就会被覆盖,有问题!!!std::cout << name << " is running" << std::endl;sleep(1);}return nullptr;
}
int main()
{// 问题6 : 如何创建多线程呢?std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1.有线程的idpthread_t tid;// 2.有线程的名字char name[128];snprintf(name, sizeof(name), "thread-%d", i + 1);pthread_create(&tid, nullptr, threadrun, /*线程的名字*/ name);}// join todosleep(100);return 0;
}

运行结果  

正确示范

只需让name在堆区创建即可,并修改格式化name函数

// 2.有线程的名字(正确示范)
char* name  = new char[128];
snprintf(name, 128, "thread-%d", i + 1);

等待(终止)多线程

创建好新线程之后,保存每个线程的tid,遍历vector终止新线程即可!

代码演示  

const int num = 10;void *threadrun(void *args)
{std::string name = static_cast<const char *>(args);while (true){// 打印的线程名是乱的,线程执行顺序是不确定的,// 且因为在名字栈区for循环内部创建,每切换一个线程,名字就会被覆盖,有问题!!!std::cout << name << " is running" << std::endl;sleep(1);break;}// return nullptr;return args;
}
int main()
{// 问题6 : 如何创建多线程呢?std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1.有线程的idpthread_t tid;// 2.有线程的名字(错误示范)// char name[128];// snprintf(name, sizeof(name), "thread-%d", i + 1);// 2.有线程的名字(正确示范)char* name  = new char[128];snprintf(name, 128, "thread-%d", i + 1);pthread_create(&tid, nullptr, threadrun, /*线程的名字*/ name);// 3.保存所有线程的id信息tids.emplace_back(tid);}// join todofor(auto tid : tids){void* name = nullptr;pthread_join(tid,&name);// std::cout << PrintToHex(tid) << " quit" << std::endl;std::cout << (const char*)name << " quit" << std::endl;delete (const char*)name;}// sleep(100);return 0;
}

tid方式打印

运行结果  

线程名方式打印

运行结果  

相关文章:

【Linux系统编程】第四十二弹---多线程编程全攻略:涵盖线程创建、异常处理、用途、进程对比及线程控制

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、线程创建 2、线程异常 3、线程用途 4、进程 VS 线程 5、线程控制 5.1、创建和等待线程 1、线程创建 线程能看到进程的大…...

Rust 力扣 - 2379. 得到 K 个黑块的最少涂色次数

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 本题可以转换为求长度为k的子数组中白色块的最少数量 我们遍历长度为k的窗口&#xff0c;我们只需要记录窗口内的白色块的数量即可&#xff0c;遍历过程中刷新白色块的数量的最小值 题解代码 impl Solution {…...

“单元测试”应该怎么写比较好

如何正确写单元测试 单元测试重要性写单元测试时存在的问题1、如何命名测试类&方法1.1、测试类命名规范1.2、测试方法命名规范 2、测试类的要求2.1测试行覆盖率100%2.2、单一职责2.3、可重复2.4、外部隔离&#xff0c;无任何外部依赖2.5、正确的使用断言2.6、不应该为了测试…...

腾讯开源首个文图生3D大模型Hunyuan3D-1.0

&#x1f989; AI新闻 &#x1f680; 腾讯开源首个文图生3D大模型Hunyuan3D-1.0 摘要&#xff1a;腾讯混元于11月5日开源了Hunyuan3D-1.0&#xff0c;这是首个同时支持文本生成和图像生成的3D开源大模型。该模型采用两阶段生成方法&#xff0c;可以在10秒内生成3D资产。第一阶…...

c语言简单编程练习10

1、typedef和#define的区别 在用作数据类型替换时的区别&#xff1a; #include <stdio.h> #include <unistd.h>typedef char * A; //typedef需要&#xff1b; #define B char *int main(int argc, char *argv[]) {A a,b;B c,d;printf("a_size%ld\n"…...

时序预测 | Matlab基于TSA-LSTM-Attention被囊群优化算法优化长短期记忆网络融合注意力机制多变量多步时间序列预测

时序预测 | Matlab基于TSA-LSTM-Attention多变量多步预测 目录 时序预测 | Matlab基于TSA-LSTM-Attention多变量多步预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 时序预测 | Matlab基于TSA-LSTM-Attention被囊群优化算法优化长短期记忆网络融合注意力机制多变量多…...

数据结构[2016]

一、设有二维数组A[6][8],每个元素占6个字节存储&#xff0c;实现存放&#xff0c;A[0][0]的起始地址为1000,计算&#xff1a; (10分) (1)数组最后一个元素A[5][7]的起始地址&#xff1b; (2)按行优先存放时&#xff0c;元素A[1][4]的起始地址&#xff1b; (3)按列优先存放时…...

DBAPI连接阿里云 maxcompute 报错

使用正确的驱动包 访问以下链接寻找驱动包 https://github.com/aliyun/aliyun-odps-jdbc/releases/tag/v3.4.3 注意要使用odps-jdbc-3.4.3-jar-with-dependencies.jar &#xff0c;这个是完整的jar包 不要使用odps-jdbc-3.4.3.jar&#xff0c;这个不是完整的&#xff0c;它还…...

Web3对社交媒体的影响:重新定义用户互动方式

随着互联网的发展和人们对隐私、安全、所有权的需求不断提高&#xff0c;Web3 的概念逐渐深入人心。Web3 的出现标志着一个去中心化、用户主导的网络时代的到来&#xff0c;这也将对社交媒体产生深远的影响。Web3 不仅推动社交媒体从中心化模式向用户主导的去中心化模式转变&am…...

【LeetCode】【算法】322. 零钱兑换

LeetCode 322. 零钱兑换 题目 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回-1。 你可以认为每…...

人工智能技术:未来生活的“魔法师”

想象一下&#xff0c;未来的某一天&#xff0c;你醒来时&#xff0c;智能助手已经为你准备好了早餐&#xff0c;你的智能家居系统根据你的心情和日程安排调整了室内的光线和音乐&#xff0c;而你的自动驾驶汽车已经在门口等你。这不是科幻小说&#xff0c;这是人工智能技术为我…...

docker加载目录中所有的镜像

docker加载目录中所有的镜像 首先我们知道读取单个命令如下: docker load -i example_image.tar.gz读取两三个也是: docker load -i image1.tar.gz image2.tar.gz image3.tar.gz但是如果是几十个&#xff0c;那么上面的命令就显得捉襟见肘了&#xff1b;比如当前我有个image…...

使用免费的飞书机器人,实现消息推送实时通知

大家好&#xff0c;我是小悟。 实际工作中&#xff0c;我们会经常遇到需要给用户发送业务通知的功能需求&#xff0c;如果是小程序端&#xff0c;那么就使用小程序提供的模板消息通知&#xff0c;如果是APP端&#xff0c;一般就是使用个推、极光等第三方平台。 当然还有个万能…...

各种网络设备的工作原理

网络设备的工作原理涉及多种设备&#xff0c;包括路由器、交换机、防火墙等&#xff0c;它们各自承担着不同的功能。以下是对这些设备工作原理的详细解释&#xff1a; 一、路由器路由器是互联网通信中的关键设备&#xff0c;它负责在不同网络之间传输数据包。功能&#xff1a;路…...

FilterListener组件

文章目录 Java Web三大组件一、Filter概述二、Filter开始1_过滤器API介绍2_过滤器开发步骤3_代码实现4_过滤器执行流程小结 三、使用细节1_生命周期2_拦截路径3_过滤器链 四、Listener1_Listener概述2_监听器举例3_Listener开始4_案例:模拟spring框架 Java Web三大组件 组件: 是…...

使用Ubuntu快速部署MinIO对象存储

想拥有自己的私有云存储&#xff0c;安全可靠又高效&#xff1f;MinIO是你的理想选择&#xff01;这篇文章将手把手教你如何在Ubuntu 22.04服务器上部署MinIO&#xff0c;并使用Nginx反向代理和Let’s Encrypt证书进行安全加固。 即使你是新手&#xff0c;也能轻松完成&#xf…...

基于Liquid State Machine的时间序列预测:利用储备池计算实现高效建模

Liquid State Machine (LSM) 是一种 脉冲神经网络 (Spiking Neural Network, SNN) ,在计算神经科学和机器学习领域中得到广泛应用,特别适用于处理 时变或动态数据。它是受大脑自然信息处理过程启发而提出的一种 脉冲神经网络 。 设想你正处于一片平静的湖面,四周环绕着高山,你向…...

oracle使用CTE递归分解字符串

oracle使用CTE递归分解字符串 背景 给定一个不定长度字符串 并且以&#xff0c;分割例如 ‘1&#xff0c;2&#xff0c;3&#xff0c;4’ 使用sql查询 返回1&#xff0c;2&#xff0c;3&#xff0c;4四行 如果‘1&#xff0c;2’ 则返回 1&#xff0c;2 两行 使用sql实现 实…...

华为HarmonyOS借助AR引擎帮助应用实现虚拟与现实交互的能力5-识别平面语义

对于检测到的平面&#xff0c;您可以通过AR Engine识别该平面的语义&#xff0c;包括墙面、地面、座椅面、桌面、天花板、门面、窗面、床面。 创建AR会话 创建AR会话并配置为平面语义识别模式。 AREngine_ARSession *arSession nullptr;// 创建AR会话。HMS_AREngine_ARSessi…...

MAC 安装 brew及其常用命令

​文章&#xff1a;Mac安装brew的四种方法&#xff08;指定能行&#xff09; 以下是在 Mac 上使用 Homebrew 清理缓存和无用包的详细指南&#xff1a; 1. 查看系统状态 # 诊断系统问题 brew doctor# 查看已安装的包 brew list# 查看系统占用空间 brew cleanup -n # 预览需要…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

Java多线程实现之Callable接口深度解析

Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

P3 QT项目----记事本(3.8)

3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。

1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj&#xff0c;再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

什么是Ansible Jinja2

理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具&#xff0c;可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板&#xff0c;允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板&#xff0c;并通…...