【Linux初阶】多线程2 | 分离线程,线程库,线程互斥,可重入VS线程安全,锁的常见概念

news2024/10/7 6:46:12

在这里插入图片描述

文章目录

  • ☀️一、分离线程
    • 🌻1.pthread_ self - 获取线程ID
    • 🌻2.线程分离
  • ☀️二、用户级线程库
    • 🌻1.pthread_t
    • 🌻2.理解用户级线程库 - pthread库
    • 🌻3.局部存储
  • ☀️三、线程互斥
    • 🌻1.线程间的互斥相关概念
    • 🌻2.互斥量 - mutex
    • 🌻3.互斥量接口
      • ⚡(1)初始化互斥量
      • ⚡(2)销毁互斥量
    • 🌻4.互斥量原理探究
  • ☀️四、可重入VS线程安全(了解)
  • ☀️五、锁的常见概念
    • 🌻1.锁的基础概念
    • 🌻2.死锁(了解)
      • ⚡(1)死锁四个必要条件
      • ⚡(2)避免死锁
      • ⚡(3)避免死锁算法


☀️一、分离线程

🌻1.pthread_ self - 获取线程ID

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面(上一篇文章)说的线程ID不是一回事。

  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。

  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

pthread_t pthread_self(void);

🌻2.线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行 pthread_join操作(线程等待),否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,即分离线程操作

线程分离的代码如下:

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void* thread_run(void* arg)
{
	pthread_detach(pthread_self());
	printf("%s\n", (char*)arg);
	return NULL;
}

int main(void)
{
	pthread_t tid;
	if (pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0) {
		printf("create thread error\n");
		return 1;
	}

	int ret = 0;
	pthread_detach(tid);	//我们可以直接在主线程对新线程进行分离
	sleep(1); //也可以让主线程休眠一下,先让新线程分离(我们不知道主线程等待和新线程分离谁先运行,所以sleep1秒)


	if (pthread_join(tid, NULL) == 0) {
		printf("pthread wait success\n");
		ret = 0;
	}
	else {
		printf("pthread wait failed\n");
		ret = 1;
	}

	return ret;
}

运行结果:屏幕输出 - pthread wait failed,即等待失败

总结:当我们不关心新线程返回值时,可以使用线程分离操作,这样我们就不用对新线程进行等待


☀️二、用户级线程库

🌻1.pthread_t

  • pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
  • 接下来我们会对这部分知识做深入讲解。

🌻2.理解用户级线程库 - pthread库

  • 图一:

在这里插入图片描述

  • 用户需要通过用户级线程库创建新线程,线程库使用的接口为 clone
  • pthread库也被称为原生线程库
  • 创建新线程:库会帮我们创建对应的线程PCB(轻量级进程)、TCB(线程控制块),然后将新线程的数据压到库空间中

小结:1.用户级线程,用户关心的线程属性在库中,内核提供线程执行流的调度。2.用户级线程:内核轻量级进程 = 1:1。

  • 原生线程库中,可能存在多个线程。这些线程也需要我们管理起来

  • 我们通过“先描述,再组织”的方式对新线程进行管理。具体方法我们在后面进行详细讲解。

  • 图二:

在这里插入图片描述

  • 磁盘中保存有 libpthread.so库文件,它对应在地址空间的映射区域为共享区(mmap区域)

  • 图三:
    在这里插入图片描述

  • 上图中用蓝色框框住的,就是一个个线程控制块(TCB),我们可以将它们以数组(TCB threads[ ])的形式排列起来

  • pthread_t tid 就是一个进程控制块的起始地址!

  • 通过上图我们可以印证,每个线程都有自己的上下文(局部存储)和栈(线程栈)

  • 新线程的位置位于共享区的mmap区域中,即主线程的栈保存在地址空间的栈空间中,新线程的栈保存在共享区中

先描述:我们通过线程控制块将新线程的数据进行描述。

再组织:就是使用数组的方式将线程的属性信息按需陈列在我们的库当中,再使用地址来标识某个线程,未来用户只要知道某个线程的线程ID(地址),就能对某个线程进行操作了。

🌻3.局部存储

我们可以通过添加__thread,将全局变量设置为线程局部存储

__thread int g_val = 100
  • 设置为局部存储之后,这个变量依旧为全局变量,但是它会在每个线程中都保存一份
  • 在新线程中对该变量做修改,不会影响主线程该变量对应的值
  • 该变量在新线程中会被保存在它的私有栈中,即共享区中

☀️三、线程互斥

🌻1.线程间的互斥相关概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源。
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

🌻2.互斥量 - mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;

void* route(void* arg)
{
	char* id = (char*)arg;
	while (1) {
		if (ticket > 0) {
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
		}
		else {
			break;
		}
	}
}

int main(void)
{
	pthread_t t1, t2, t3, t4;
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
}

一次执行结果:
thread 4 sells ticket : 100
...
thread 4 sells ticket : 1
thread 2 sells ticket : 0
thread 1 sells ticket : -1
thread 3 sells ticket : -2
  • 正常来说ticket为0就应该已经结束才对,为什么没有输出正确结果呢?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。

  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。

  • --ticket 操作本身就不是一个原子操作。

取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
  • -- 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中。

  • update : 更新寄存器里面的值,执行-1操作。

  • store :将新值,从寄存器写回共享变量ticket的内存地址。

  • 由此可见,原子性的代码编译之后应该只有一条汇编指令

当多个线程同时对某个临界资源进行操作、读取、判断,就可能会出现内存数据不一致的问题。依旧用上面的代码举例,你线程A已经将值减到0,此时另一线程B已经处于判断完的阶段,线程B会继续--,导致最后出现-1。

多线程交替执行还可能造成下面的问题:可能线程A刚完成第二步,我们在这里假设它--到了99,线程A的时间片到了,线程A被剥离,线程A在被操作系统剥离时会将CUP寄存器中的值也带走,而后换上线程B进行执行。线程B--到了20,并将这个值写回了内存,此时它的时间片也到了,重新换上线程A执行。此时线程A会恢复寄存器中的值(99),并执行第三步,将这个值加载到内存,此时内存中线程共有的 ticket值就被覆盖了(20->99),从而影响线程的下一次判断。

我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面那样多个线程交替式执行,会造成数据不一致问题

  • 要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把。Linux上提供的这把锁叫互斥量

在这里插入图片描述

🌻3.互斥量接口

⚡(1)初始化互斥量

初始化互斥量有两种方法:

  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

注意:静态分配互斥量即在全局中进行分配,静态分配之后不需在进行 init动态分配和 destroy销毁

  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
	mutex:要初始化的互斥量
	attr:NULL

⚡(2)销毁互斥量

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

销毁互斥代码如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex)

互斥量加锁和解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_mutex_lock 进行加锁时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功(成功加锁)。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁(加锁失败,线程阻塞)。

改进上面的售票系统:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

int ticket = 100;
pthread_mutex_t mutex;

void* route(void* arg)
{
	char* id = (char*)arg;
	while (1) 
	{
		pthread_mutex_lock(&mutex);
		if (ticket > 0) 
		{
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);

		}
		else {
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
}

int main(void)
{
	pthread_t t1, t2, t3, t4;

	pthread_mutex_init(&mutex, NULL);

	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");

	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);

	pthread_mutex_destroy(&mutex);
}

一次执行结果:
thread 4 sells ticket : 100
...
thread 3 sells ticket : 1
thread 3 sells ticket : 0

总结:加锁之后,可让线程进行串形执行

🌻4.互斥量原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者++i都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(如下面代码 xchgb),该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
  • 现在我们把lock和unlock的伪代码表述一下

在这里插入图片描述

加锁理解:

  • 假设有线程A、B两个线程。
  • 线程A将0加载到 %al寄存器中。
  • 内存中有一空间专门存储 mutex变量mutex变量默认为1,代表锁未被申请。xchgb将寄存器中的值和内存中的mutex进行交换。
  • 如果al寄存器中的值大于0,则返回并执行相应的内容代码,否则就将线程挂起。
  • 如果线程A在使用锁,在线程A时间片到了被剥离下来的时候它会将寄存器中的值也带走。
  • 线程B进入该代码逻辑后,从内存中交换到al寄存器寄存器中的值等于0,经过判断,最终被挂起。

解锁理解:

  • al寄存器中的值重新 movb回内存中,将释放锁(将mutex恢复为默认值)。

☀️四、可重入VS线程安全(了解)

  • 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,就会出现线程不安全问题。

  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

  • 常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  • 类或者接口对于线程来说都是原子操作

  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

  • 常见的线程不安全的情况

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

  • 常见可重入的情况

  • 不使用全局变量或静态变量

  • 不使用用malloc或者new开辟出的空间

  • 不调用不可重入函数

  • 不返回静态或全局数据,所有数据都有函数的调用者提供

  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

  • 常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

  • 可重入与线程安全区别

  • 可重入函数是线程安全函数的一种线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。


☀️五、锁的常见概念

🌻1.锁的基础概念

  • 锁在Linux下是互斥量的别称,用于保护临界区资源,使临界区资源只能由线程串行访问。

  • 加锁和解锁的过程是由多个线程串行执行的,即使某个线程的时间片到了,被剥离下来也是带着锁的,其他线程不能执行锁中间的代码(锁其实是原子的)。

  • 使用锁会导致程序的运行速度会变慢。

  • 锁只规定互斥访问,没有规定那个线程优先执行。

  • 锁在多线程中是需要多个线程进行竞争的。

  • 未来我们在使用锁的时候,一定要保证临界区的粒度(锁中间代码量)要非常小!

  • 加锁成功函数会返回特定的值,失败会导致线程阻塞、被挂起(锁正在被占用)。

🌻2.死锁(了解)

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
  • 简单来说,就是在多把锁的情况下,我们持有自己的锁不释放,还要去申请对方的锁,对方也是如此,最终大家都在永久等待,造成死锁。、
  • 一把锁也可能造成死锁(代码写错了,一把锁申请两次)。
  • 任何技术都是有边界的,技术在解决某些问题的同时往往会引入新的问题。

⚡(1)死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

⚡(2)避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

⚡(3)避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1089622.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kali】简单记录

文章目录 信息收集DNS记录分析hostdigdnsenum 路由信息tcptraceroutetctrace 搜索引擎 目标识别arpingfping 识别操作系统p0f 服务枚举端口扫描nmap识别VPN服务器 漏洞映射exploitdbmsfconsole 提权arpspoofDsniff 信息收集 DNS记录分析 host host www.example.com host -a …

Windows 多媒体编程库 DirectX 介绍

目录 1、什么是DirectX&#xff1f; 2、使用DirectX的好处 2.1、DirectX为软件开发者提供硬件无关性 2.2、为硬件开发提供策略 3、DirectX的主体构成 3.1、Direct3D 3.2、DirectDraw 3.3、DirectPlay 3.4、DirectSound 3.5、DirectMusic 3.6、DirectInput 4、Dire…

Python数据分析实战-实现T检验(附源码和实现效果)

实现功能 T 检验&#xff08;Students t-test&#xff09;是一种常用的统计方法&#xff0c;用于比较两个样本之间的均值是否存在显著差异。它可以应用于许多场景&#xff0c;其中一些常见的应用场景包括&#xff1a; A/B 测试&#xff1a;在市场营销和用户体验研究中&#xf…

【mmdetection代码解读 3.x版本】FPN层的解读

文章目录 1. forward函数1.1 self.fpn_convs的构建 1. forward函数 def forward(self, inputs: Tuple[Tensor]) -> tuple:assert len(inputs) len(self.in_channels)# build laterals laterals [lateral_conv(inputs[i self.start_level])for i, lateral_conv in enumer…

QMidi Pro for Mac:打造您的专属卡拉OK体验

你是否曾经厌倦于在KTV里与朋友们争夺麦克风&#xff1f;是否想要在家中享受自定义的卡拉OK体验&#xff1f;现在&#xff0c;有了QMidi Pro for Mac&#xff0c;一切变得简单而愉快&#xff01; QMidi Pro是一款功能强大的卡拉OK播放器&#xff0c;专为Mac用户设计。它充分利…

机器学习(二)什么是机器学习

文章目录 什么是机器学习1.4.1确定是否为机器学习问题 1.5基于规则学习和基于模型的学习1.5.1基于规则学习1.5.2基于模型学习1.5.3房价预测问题 1.6机器学习数据的基本概念1.6.1机器学习数据集基本概念强化实践 后记 什么是机器学习 在开始讲解术语概念之前我们首先梳理下之前…

互联网Java工程师面试题·Java 并发编程篇·第三弹

目录 26、什么是线程组&#xff0c;为什么在 Java 中不推荐使用&#xff1f; 27、为什么使用 Executor 框架比使用应用创建和管理线程好&#xff1f; 27.1 为什么要使用 Executor 线程池框架 27.2 使用 Executor 线程池框架的优点 28、java 中有几种方法可以实现一个线程…

天猫用户重复购买预测(速通二)

天猫用户重复购买预测&#xff08;二&#xff09; 模型训练分类相关模型1、逻辑回归分类模型2、K近邻分类模型3、高斯贝叶斯分类模型4、决策树分类模型5、集成学习分类模型 模型验证模型验证指标 特征优化特征选择技巧1、搜索算法2、特征选择方法 模型训练 分类相关模型 1、逻…

基于Springboot实现商务安全邮箱邮件收发系统项目【项目源码+论文说明】

基于Springboot实现商务安全邮箱邮件收发系统演示 摘要 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。计算机的优势和普及使得商务安全邮箱的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;采用jsp技术…

Altium Designer | 5 - 网表导入及模块化布局设计(待续)

导入常见报错解决办法(unknow pin及绿色报错等) 在原理图界面 CtrlF搜索元器件位号 在PCB界面&#xff0c;CtrlF是左右翻转&#xff0c; 快捷键JC才是搜索元器件位号 报错信息&#xff1a; Unknow pin 1.没有封装 2.封装管脚缺失 3.元件库对应的管脚不对 ... 常见绿色报…

一键部署开源AI(人工智能对话模型)(支持显卡或CPU加内存运行)--ChatGLM2-6B

一、基本介绍&#xff1a; ChatGLM2-6B 是开源中英双语对话模型 ChatGLM-6B 的第二代版本&#xff0c;在保留了初代模型对话流畅、部署门槛较低等众多优秀特性的基础之上&#xff0c;ChatGLM2-6B 引入了如下新特性&#xff1a; 更强大的性能&#xff1a; 基于 ChatGLM 初代模…

盘点15个前端开源项目,yyds!

目录 1、vue-color-avatar2、Reader3、Ant Design4、小游戏2048&#xff08;Vue版&#xff09;5、跳一跳6、lifeRestart&#xff08;人生重开模拟器&#xff09;7、GOVIEW8、vlife9、网易云音乐 API10、饿了么11、QQ音乐 API12、ChatGPT API13、Node.js 最佳实践14、Awesome No…

云计算革命:多云管理与混合云的实践指南

文章目录 云计算的演进多云管理的优势1. 降低风险2. 提高性能3. 降低成本4. 提高安全性 实践指南1. 选择适当的云提供商2. 使用云管理平台3. 实施一致的安全策略4. 数据管理和迁移5. 自动化和编排 混合云的实践1. 私有云和本地数据中心2. 数据一致性3. 安全性和合规性4. 负载均…

IDEA启动报错Failed to create JVM. JVM path的解决办法

今天启动IDEA时IDEA报错&#xff0c;提示如下。 if you already hava a JDK installed, define a JAVA_HOME variable in Computer > Systen Properties > System Settings > Environment Variables.Failed to create JVM. JVM path:D:\ideaIU2023.2.3\IntelliJ IDE…

表单页面风格如何选择?弹窗 or 抽屉 or 页面?

一、类型介绍 在 PC 端项目中,用户触发了某个操作,当需要向用户展示新的内容时,有很多交互方式,弹窗、抽屉、页面就是其中典型的3种。下面来分析下3种交互方式的优势、劣势和使用场景。 1.1 弹窗 定义:分为模态和非模态对话框2种,常用的为模态对话框。 优势:在不离开…

AI项目十六:YOLOP 训练+测试+模型评估

若该文为原创文章&#xff0c;转载请注明原文出处。 通过正点原子的ATK-3568了解到了YOLOP&#xff0c;这里记录下训练及测试及在onnxruntime部署的过程。 步骤&#xff1a;训练->测试->转成onnx->onnxruntime部署测试 一、前言 YOLOP是华中科技大学研究团队在2021年…

2023软件测试面试题(亲身经历)

在职&#xff0c;5年测试经验&#xff0c;坐标广州&#xff0c;有点想666。于是进行了几场线上面试… 1、python有哪些数据类型 数字型&#xff1a;int/float/bool/complex 字符串&#xff1a;str 列表&#xff1a;list 元组&#xff1a;tuple 字典&#xff1a;dict 集合&…

云原生应用安全性:解锁云上数据的保护之道

文章目录 云原生应用的崛起云原生应用安全性挑战1. **容器安全性**&#xff1a;容器技术如Docker和Kubernetes已成为云原生应用的核心组成部分。容器的安全性变得至关重要&#xff0c;以防止恶意容器的运行和敏感数据泄漏。2. **微服务安全性**&#xff1a;微服务架构引入了多个…

从零开始使用webpack搭建一个react项目

先做一个正常编译es6语法的webpack demo 1. 初始化package.json文件 npm init一路enter下去 2. 添加插件 {"name": "demo","version": "1.0.0","description": "","main": "index.js",&q…

Springboot利用CompletableFuture异步执行线程(有回调和无回调)

目录 背景 实现 一、异步线程配置类 二、自定义异步异常统一处理类 三、实现调用异步&#xff08;无回调-runAsync()&#xff09; 四、实现调用异步&#xff08;有回调-supplyAsync()&#xff09; 五、异步执行错误异常示例 背景 项目中总会有需要异步执行来避免浪费…