【关于Linux中----多线程(一)】

文章目录

  • 认识线程
  • 创建线程
  • 线程优点和缺点
  • 创建一批线程
  • 终止线程
  • 线程的等待问题


认识线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

在这里插入图片描述

上面这张图中出现的内容都已经在前面的文章中解释过,这里再对页表内容做一个补充。

我们都知道,在语言中,定义在常量区中的变量是不可被修改的,为什么?

答:常量区存在于已初始化数据和代码区之间。当修改常量区中的内容时,要先找到变量所在的地址,再经过页表映射到具体的物理内存进行操作。但是在页表映射时,页表会检查RWX权限和要进行的操作。常量区的变量所具有的权限是R,所以自然不能进行写操作。MMU就会向硬件报错,进而被OS所识别,然后会将报错转变成信号(段错误)发送给进程,然后进程会以默认动作(终止)来处理该进程。

如何看待进程地址空间和页表?

  • 地址空间是进程能看到的资源窗口
  • 页表决定进程真正拥有资源的情况
  • 合理地对页表和地址空间进行资源划分就可以对一个进程所有的资源分类

进程地址空间是如何经过页表找到具体的物理内存的?

首先,OS要对物理内存进行管理,就要把物理内存分成一个个的物理页(每一个物理页大小为4KB),每一个物理页都有描述自己属性的结构体,然后OS会用数组的方式来组织起所有的物理页,进而管理整个物理内存。
而内存中的数据与物理内存进行交互时,传输数据的单位也是4KB(这样的一块数据在磁盘中叫做页帧)。最后,OS会用特定的管理算法对所有物理页进行管理。

以32为平台为例,每一个虚拟地址都有32个比特位。而虚拟地址经过页表映射时,不是将32个比特位直接映射到物理内存,而是将虚拟地址划分为10个、10个和12个比特位进行三次映射,最后到达具体的物理内存

页表包括页目录和页表项:

在这里插入图片描述
前10个比特位一共有1024个组合,所以页目录里也会有1024个与之对应的位置,页表项也是如此。

现根据前十个比特位在页目录中找到具体的页表项,再根据下一组十个比特位在页表项中找到物理内存中的具体某一个物理页,最后根据后十二个比特位(相对物理页起始位置的偏移量)找到数据在物理页中的具体位置。
而每一个物理页的大小是4KB,也就刚好对应后12个比特位可能出现的组合的数量。

以上是对进程概念的补充。在我之前的文章中解释的进程就是内核数据结构+进程对应的代码和数据。而现在要说的线程就是进程内的一个执行流

如何理解“线程是进程内的一个执行流”?

上文中说了“虚拟内存决定了进程能看到的资源”,而当我们用fork创建子进程时,可以让父进程和子进程分别执行不同的操作。

那么如果我们现在创建一批子进程,但是只给这些子进程创建属于自己的PCB,而不给它们创建进程地址空间和页表等,并把它们的PCB指向和父进程一样的进程地址空间,再把进程地址空间中的各个区域划分给不同的子进程供其使用(同时将部分资源划分给子进程)。这时,这些子进程就相当于父进程中的执行流,而我们也可以认为新创建出的一个个PCB就是一个个线程。

而因为我们可以通过进程地址空间+页表的方式对进程进行资源划分,所以单个线程的执行力度一定会比原来的进程更细节。


创建线程

如果OS要设计线程的概念,那么要不要对线程进行管理?如何管理?

答:如果有线程的概念,就一定要想办法对其进行管理,而管理的方法就像进程一样,要为它设计专门的数据结构来表示一个线程对象,再把它们组织起来。而Windows中就是这么做的,其中描述线程的数据结构叫做TCB

可是,如果有了线程的概念和数据结构,当它被调度执行的时候,就一定需要ID 状态 优先级 上下文 栈等概念,这样看来,线程和进程有很多方面都是重复的。所以早期的Linux工程师们就不再专门设计“线程”的概念,而是直接对进程中的PCB进行复用,就成了我们现在所说的“线程”

所以,Linux中根本就不存在线程的概念。在Linux中,进程就是承担分配操作系统资源的的基本实体,而线程是CPU调度的基本单位。一个进城内部可以有多个执行流,而认为单个进程内部只有一个执行流。

下面用代码证明一下以上内容:

首先要了解创建线程的函数:
在这里插入图片描述

代码如下:
在这里插入图片描述
对应的Makefile内容如下:
在这里插入图片描述

运行结果如下:

[sny@VM-8-12-centos practice]$ make clean;make
rm -f mythread
g++ -o mythread mythread.cpp
/tmp/ccLOh0U0.o: In function `main':
mythread.cpp:(.text+0x53): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
make: *** [mythread] Error 1
[sny@VM-8-12-centos practice]$ 

出现这种情况的原因是,找不到创建线程所依赖的库。
因为Linux没有为创建线程专门的系统调用接口,所以只能依赖库来实现,解决方案如下:

在这里插入图片描述
(这部分在前面动静态库的文章中有解释)

[sny@VM-8-12-centos practice]$ vim Makefile
[sny@VM-8-12-centos practice]$ make clean;make
rm -f mythread
g++ -o mythread mythread.cpp -lpthread
[sny@VM-8-12-centos practice]$ ls
core.30230  Makefile  mytest  mythread  mythread.cpp
[sny@VM-8-12-centos practice]$ 

运行成功!

程序所连接到的库如下:

[sny@VM-8-12-centos practice]$ ldd mythread
	linux-vdso.so.1 =>  (0x00007ffcc13dc000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f0b44956000)
	libstdc++.so.6 => /home/sny/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6 (0x00007f0b445d5000)
	libm.so.6 => /lib64/libm.so.6 (0x00007f0b442d3000)
	libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f0b440bd000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f0b43cef000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f0b44b72000)
[sny@VM-8-12-centos practice]$ ls /lib64/libpthread.* -al
-rw-r--r-- 1 root root 152194 May 19  2022 /lib64/libpthread.a
-rw-r--r-- 1 root root    222 May 18  2022 /lib64/libpthread.so
lrwxrwxrwx 1 root root     18 Jul 25  2022 /lib64/libpthread.so.0 -> libpthread-2.17.so
[sny@VM-8-12-centos practice]$ 

libpthread-2.17.so这个库叫做用户线程库/原生线程库,任何Linux系统都必须含有,在其内部实际上含有系统调用接口。

对于上面的代码,如果两个线程在一个执行流中,就不可能同时运行两个while循环。所以,根据运行结果就可以验证“线程是进程中的一个执行流”以及“一个进程中可以有多个执行流”的说法,结果如下:
在这里插入图片描述

通过运行结果可以里看到,虽然有两个线程在跑,但系统中只有一个进程,也就是说两个执行流在同一个进程内,当kill掉这个进程之后,两个线程都被终止了。
那么怎么查看两个执行流具体的信息?

方法如下:
在这里插入图片描述
执行ps -aL命令可以看到连个线程。
上面两个线程PID相同,但是LWP不相同,LWP即为light weight process,也就是轻量级进程

另外,可以看到第一个线程的LWP和PID是相同的!!!这表明第一个线程为主线程,第二个线程为新线程。

所以,CPU进行调度时,是以LWP为标识符表示一个特定的执行流的(只不过之前代码中每个进程只有一个执行流,所以PID和LWP没区别)

下面来验证一下pthread_create的第四个参数是第三个参数指向的函数的参数这句话,对代码稍作改动,如下:
在这里插入图片描述
结果如下:

[sny@VM-8-12-centos practice]$ make clean;make
rm -f mythread
g++ -o mythread mythread.cpp -lpthread
[sny@VM-8-12-centos practice]$ ./mythread
主线程running!新线程running! name : thread one
主线程running!
新线程running! name : thread one
新线程running! name : 主线程running!thread one
^C
[sny@VM-8-12-centos practice]$ 

接下来看一下pthread_create的第一个参数(tid)的值:
在这里插入图片描述
结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
主线程running! tid: 新线程running! name : thread one140114343909120

主线程running! tid: 140114343909120
新线程running! name : thread one
主线程running! tid: 140114343909120新线程running! name : 
thread one

这一长串数字显然看不懂什么意思,用十六进制输出一下试试:
在这里插入图片描述
结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
主线程running! tid: 0xdb2f1700新线程running! name : thread one
新线程running! name : thread one主线程running! tid: 0xdb2f1700
主线程running! tid: 0xdb2f1700新线程running! name : 
thread one

现在还是看不懂,暂且先把这个问题放在这儿,下文再做解释。

现在开始一个新的话题:线程一旦被创建,几乎所有的资源都是被所有的线程所共享的。也就是说,一个进程中的不同执行流可以看到彼此所能看到的资源,当其中一个执行流更改数据时,其他执行流也可以看到更改后的数据。(感兴趣的可以自己写一小段代码验证一下。)所以,线程之间进行数据交换和通信是非常容易的。

但是线程之间也有自己私有的资源,包括:

  • PCB属性
  • 上下文结构
  • 线程独立的栈结构

线程优点和缺点

优点:
①创建一个新线程的代价要比创建一个新进程小得多

这一点在上文中已经解释过了,这里不再赘述。

②与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

原因如下:
第一,进程之间切换时,需要切换的内容有页表、虚拟地址空间、PCB和上下文;但进程之间相互切换只需要切换PCB和上下文。

这样看起来,进程和线程切换效率并没有很大差别,那它们的效率差距体现在哪儿呢?

这里需要补充一个cache的概念。
在这里插入图片描述
cache是存在于CPU中的一个硬件缓存,它的运行速率比CPU慢,但是远高于内存。其中存储的主要是一些当前执行流经常用到以及很可能用到的热点数据,目的是当执行流需要读取这些数据时可以更快速地完成。
由于线程之间大部分资源是共享的,所以切换时也就不用切换cache中的数据;但是进程切换却要将cache中的数据全部换掉,太频繁的切换甚至会导致cache失效,效率就会大大降低,这就是差距所在。

③线程占用的资源要比进程少很多
④能充分利用多处理器的可并行数量
⑤在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
⑥计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
⑦I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

这几点很好理解,不再赘述。

缺点:

① 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
②健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

③缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

⑤编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

这里解释一下什么是“线程健壮性”:
在我们上面的代码中一共有两个线程,那么如果其中一个线程出问题,会不会影响另一个线程?
用代码测试一下:
在这里插入图片描述

可以看到代码中有一个很明显的错误,运行结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
主线程running! tid: 0x365f7700新线程running! name : thread one
主线程running! tid: 0x365f7700
Segmentation fault

可以看到,两个线程都被终止了。
上面的现象就说明线程的健壮性较差!因为信号是发送给进程整体的,当操作系统检测到异常时,会向进程发送信号,由于每一个人线程的PID都是一样的,所以都会对信号做出处理。
从资源的视角来看,操作系统向进程发送信号,随后回收进程资源。由于线程之间资源大部分共享,所以其他线程也就不存在了。

操作系统提供的创建轻量级进程的接口是什么?

在这里插入图片描述
实际上,上文中的代码中底层就调用了这个函数。

还有一个跟fork有些区别的接口:
在这里插入图片描述
它和fork的区别在于,vfork创建出来的进程具有相同的地址空间,说白了,也就是创建轻量级进程的意思,但这个不常用。


创建一批线程

上文中的代码只是创建了一个线程,这里再创建一批线程,代码如下:
在这里插入图片描述

结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
new thread create successfully,name: main thread新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
new thread create successfully,name: main thread
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
new thread create successfully,name: main thread
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
new thread create successfully,name: main thread

这时,出现的线程只有2号线程,这是有点奇怪的。
对上面的代码稍作改动,在其中加入一个sleep函数,如下:
在这里插入图片描述

再次运行,结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
新线程running! name : thread:0
新线程running! name : thread:1
新线程running! name : thread:1
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
新线程running! name : thread:2
new thread create successfully,name: main thread

可见,这次的运行结果出现了0和1号线程。为什么只有一个sleep的区别,结果却不一样了?

答:①当创建新的线程之后,新线程根据缓冲区的起始地址去执行相应的任务,而主线程依然在原先的轨道上正常地进行,注意这里的传给新线程的是缓冲区的地址。
②创建新的线程之后,几个线程的执行顺序是随机的。而如果先执行的是主线程,它就会继续创建线程。而上面说传递给新线程的是缓冲区的地址,在下一次创建线程时,就会被新的线程地址覆盖,所以最后每一个新线程拿到的都是同一个缓冲区地址。
所以就会出现以上现象。

所以上面创建线程的方式其实是错误的,下面介绍正确的方式:

在这里插入图片描述

用创建对象的方式确保每一个线程收到的缓冲区地址不会被覆盖,运行结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
新线程running! name : thread:0cnt : new thread create successfully,name: main thread3
新线程running! name : thread:1cnt : 3
新线程running! name : thread:2cnt : 3
new thread create successfully,name: main thread新线程running! name : 
thread:1cnt : 2
新线程running! name : thread:0cnt : 2
新线程running! name : thread:2cnt : 2
新线程running! name : thread:1cnt : 1
new thread create successfully,name: main thread
新线程running! name : thread:0cnt : 1
新线程running! name : thread:2cnt : 1
new thread create successfully,name: main thread
new thread create successfully,name: main thread
new thread create successfully,name: main thread

这里需要明确三点:
①start_routine函数现在在被三个线程执行,所以它是重入状态。
②因为start_routine函数可以被多个线程执行,而且不会出现其他错误,所以它是可重入函数。
③在函数体内定义的变量是局部变量,具有临时性,在多线程情况下亦是如此,所以函数中的cnt在每个线程内部都是唯一的,即不重复的。


终止线程

其实在上面的代码中,我们使用return就可以直接终止一个线程了。
但是注意,使用exit终止的不是一个线程,而是进程,即一个进程中所有的执行流。二者不可混淆!

而让单个线程退出而不影响其他线程还可以调用pthread_exit函数:
在这里插入图片描述
这个方法的效果其实跟return是一样的。这里就不演示了。


线程的等待问题

同进程一样,线程退出的时候,也是需要释放资源的,否则就会造成内存泄漏等问题。所以也是需要被等待的,否则将会造成类似于僵尸进程的结果。
而我们等待线程主要是为了获取线程的退出信息以及回收线程对应的PCB等内核资源,防止内存泄漏。这一点跟进程很相像。

而等待线程的方法是pthread_join:
在这里插入图片描述
代码实例如下:
在这里插入图片描述
执行结果如下:

[sny@VM-8-12-centos practice]$ ./mythread
新线程running! name : thread:0cnt : 新线程running! name : thread:1cnt : 33
create thread: thread:0:139960118281984successfully!
create thread: thread:1:139960109889280successfully!
create thread: thread:2:139960101496576successfully!
新线程running! name : thread:2cnt : 3
新线程running! name : thread:0cnt : 2
新线程running! name : thread:1cnt : 2
新线程running! name : thread:2cnt : 2
新线程running! name : thread:0cnt : 1
新线程running! name : thread:2cnt : 1
新线程running! name : thread:1cnt : 1
join : thread:0successfully!
join : thread:1successfully!
join : thread:2successfully!
主线程退出!

由于线程退出是一瞬间的事,所以不能观察到它的中间过程。

下面解释一下pthread_join的第二个参数void** retval:

这其实是一个输出型参数,主要用来获取线程函数退出时,返回的退出结果,也就是下面这个函数的返回值。
在这里插入图片描述

为进一步理解这个参数,对代码稍作改动:
在这里插入图片描述

注意这里编译运行的时候会报警告,因为将int强转为了void*,但是不重要,直接看运行结果:

sny@VM-8-12-centos practice]$ ./mythread
create thread: thread:0:新线程running! name : thread:0cnt : 140010619401984successfully!3
新线程running! name : thread:2cnt : 3
create thread: thread:1:140010611009280successfully!
create thread: thread:2:140010602616576successfully!
新线程running! name : thread:1cnt : 3
新线程running! name : thread:0cnt : 2
新线程running! name : thread:1cnt : 2新线程running! name : thread:2cnt : 2
新线程running! name : thread:0cnt : 1新线程running! name : 新线程running! name : thread:1cnt : 1
thread:2cnt : 
1
join : thread:0successfully! number:0
join : thread:1successfully! number:1
join : thread:2successfully! number:2
主线程退出!

其实上面的代码就是证明了pthread_join函数可以接收到线程退出时的返回值,而这个返回值就存在我们使用pthread_join函数时传入的第二个参数中


本篇结束,多线程未完待续!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高通开发系列 - linux kernel内核升级msm-4.9升级至msm-4.19(1)

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 报错和警告问题中断警告的解决方案Unknown SOC ID问题解决方法msm-vidc panic错误系统时钟功能RPM功能调试共享内存smem调试之前移植过…

SpringCloud Alibaba Nacos

文章目录第一章 SpringCloud Alibaba1.1概述1.2 主要功能1.3 组件第二章 SpringCloud Alibaba Nacos服务注册与配置中心2.1 Nacos简介2.2 安装Nacos2.3 Nacos作为服务注册中心一、服务提供者二、服务消费者三、服务注册中心对比2.4 Nacos作为服务配置中心一、Nacos基础配置项目…

【C++笔试强训】第六天

选择题 1. 解析:十进制转换为八进制就是不断的除8,取余数。十进制转换成其他进制的数就是除以进制,取余。 解析:注意printf的转换,%%只会打印一个%,所以选A。 解析:由于()的原因p先和*结合&…

某面试官分享经验:看求职者第一眼,开口说第一句话,面试结果就差不多定了,准确率高达90%以上...

我们以前分享过许多经验,但大多是站在打工人的视角上,今天给大家带来一个面试官的经验:1. 看求职者第一眼,开口说第一句话,面试结果就差不多定了,准确率高达90%以上。2. 绝不考八股文,如果问技术…

docker安装MongoBD(超详细)

一、安装docker 推荐文章:https://blog.csdn.net/Sumuxi9797926/article/details/127313307?spm1001.2014.3001.5502 二、创建主机挂载配置目录 data目录存放mongodb数据库文件,删除重启容器不会丢失 mkdir -p /docker/mongodb/data && cd …

马云回国,首谈ChatGPT

马云今天回国了,这是一个备受关注的消息。 作为中国最具代表性的企业家之一,马云在过去的二十多年里,带领阿里巴巴从一个小小的创业公司,发展成为全球最大的电商平台之一,同时也推动了中国互联网行业的发展。 他的回…

Redis高可用之持久化

目录 一、高可用 什么是高可用 二、Redis持久化 持久化功能 RDB持久化 触发条件 bgsave执行流程 AOF持久化 执行流程 命令追加 文件写入和文件同步 文件重写 文件重写流程 三、RDB和AOF的优缺点 RDB持久化的优缺点 优点 缺点 AOF持久化优缺点 四、Redis性能管…

修改Hive运⾏⽇志的存放位置

默认情况下,Hive的运⾏⽇志存放在/tmp/root/hive.log ⽬录下(root是当前⽤户登录 ⽤户名)。修改hive的⽇志存放到/export/servers/hive/logs⽬录下。1. cd /export/server/hive/conf/,找到下面的文件修改/export/server/hive/conf/hive-log4j…

STM32——毕设远程室内灯光控制系统

运程室内灯光控制系统一、功能设计二、硬件选择三、按键说明四、产品主界面展示一、功能设计 本毕设以STM32 F103C8T6为主控核心板智能灯光控制系统,开发的简单易行的智能灯光控制系统,由智能手机通过蓝牙或无线与单片机系统的蓝牙或无线模块进行通信控制单片机板子上的led进行…

Fail-Fast机制和ConcurrentModificationException并发修改异常

目录说明Fail-Fast机制Fail-Fast机制的理解如何解决Fail-Fast错误机制的问题说明 Fail-Fast机制和ConcurrentModificationException并发修改异常是我们在编写代码的时候经常遇到的问题和异常,我们需要搞清楚这个机制是什么东西并且为什么会抛出这个异常,…

【新】(2023Q2模拟题JAVA)华为OD机试 - 统计差异值大于相似值二元组个数

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:统计差异值大于相似值二元组个…

尚硅谷大数据技术Scala教程-笔记01【Scala课程简介、Scala入门、变量和数据类型、运算符、流程控制】

视频地址:尚硅谷大数据技术之Scala入门到精通教程(小白快速上手scala)_哔哩哔哩_bilibili 尚硅谷大数据技术Scala教程-笔记01【Scala课程简介、Scala入门、变量和数据类型、运算符、流程控制】尚硅谷大数据技术Scala教程-笔记02【函数式编程】…

【求助贴】临危受命,如何救火做到一半的项目?

最近分享了一个关于被临时拉去救火,项目如何起死回生的小视频,没想到引起了大家的共鸣,吐槽了自己的惨痛经历并强烈要求出一个应对策略的详细教程。这也是我职场升级打怪路上一个具有代表性的绊脚石,我当时也请教了一些资深的项目…

1978-2021年全国及各省农业总产值数据

1978-2021年全国及31省农业总产值数据 1978-2021年全国及31省农业总产值数据 1、时间:1978-2021年 2、范围:31省 3、来源:统计NJ、各省NJ、 4、缺失情况:无缺失 5、指标解释说明: 农业总产值是一定时期&#x…

Keil5----跳转定义和查找功能

一、Keil5----跳转定义 跳转定义 鼠标左键点击要查找的变量 方法1: 点击鼠标右键,功能栏中有跳转定义的选项。 方法2: 按快捷键 F12 具体操作如下图所示: 跳转结果 二、Keil5----查找功能 1. 查找功能 鼠标左键点击要查找的变…

数据在内存中的存储(深度剖析)

目录 1.数据类型介绍 1.1类型分类 2.整形在内存中的存储 2.1原码,反码,补码 2.2大小端介绍 2.3练习 3.浮点型在内存中的存储 3.1浮点数存储规则 引入: 有正负的数据可以存放在有符号的变量中 只有正数的数据可以存放在无符号的变量…

Eolink Apikit 创建/生成 API 文档

在 API 研发管理产品中,几乎所有的协作工作都是围绕着 API 文档进行的。 我们在接触了大量的客户后发现,采用 文档驱动 的协作模式会比先开发、后维护文档的方式更好,团队协作效率和产品质量都能得到提高。因此我们建议您尝试基于文档来进行工…

前脚我的 GPT4 被封,后脚收到了文心一言的邀请账号

大家好,我是二哥呀。 一早醒来,我的 ChatGPT Plus 账号就惨遭封禁,很不幸,我刚冲的 Plus 会员,用了不到一周的时间(😭)。 我没用亚洲的IP,所以网上传的那些不使用亚洲IP…

Python图像处理【11】利用反卷积执行图像去模糊

利用反卷积执行图像去模糊0. 前言1. 图像模糊检测1.1 拉普拉斯 (Laplacian) 方差阈值1.2 使用 OpenCV 执行模糊检测2. 使用 SimpleITK 反卷积滤波器实现非盲去模糊2.1 去模糊分类2.2 实现非盲去模糊3. 使用 scikit-image 执行非盲去模糊小结系列链接0. 前言 我们已经知道可以使…

c++11 标准模板(STL)(std::unordered_multimap)(三)

定义于头文件 <unordered_map> template< class Key, class T, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator< std::pair<const Key, T> > > class unordered…