链接、装载与库 (一) 线程基础

1.线程基础

线程,又叫轻量级进程,是程序执行流的最小单元。
一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成。

线程私有数据:栈、线程局部存储(TLS)、寄存器。
线程共享数据:全局变量、堆、静态变量、打开的文件、程序代码。

2.Linux多线程

fork():复制当前进程
exec():使用新的可执行映像覆盖当前可执行映像
clone():创建子进程并从当前位置开始执行

1.fork()
fork产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务共享一个写时复制(Copy on Write,COW)的内存空间。
写时复制:指两个任务可以同时自由读取内存,但任意一个任务视图对内存进行修改时,内存会复制一份提供给修改方单独使用,以免影响其他的任务使用。
2.exec()
fork负责产生本任务的镜像,必须使用exec才能启动别的新任务,由exec负责执行新的可执行文件。
3.clone()
fork和exec用于产生新任务,clone才负责产生新线程。
clone可以产生一个新的任务,从指定的位置开始执行,并共享当前进程的内存空间和文件。实际效果相当于产生一个线程。

3.同步与锁

同步:指一个线程访问数据未结束时候,其他线程不得对同一数据进行访问。

同步方法:

  1. 信号量:一个初始值为N的信号量允许N个线程并发访问。
  2. 互斥量:资源仅同时允许一个线程访问。
  3. 临界区:把临界区的锁的获取称为进入临界区,锁的释放称为离开临界区。
  4. 读写锁:共享读,独占写。
  5. 条件变量:可让许多线程同时等待某事件发生,当事件发生(条件变量被唤醒),所有线程可以一起恢复运行。

1.信号量与互斥量的区别
信号量可以在整个系统被任意线程获取并释放。互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。
2.临界区与信号量、互斥量区别
互斥量和信号量在整个系统的任意进程中都是可见的,即一个进程创建了一个信号量或者互斥量,另一个进程可以试图获取改该锁。
临界区的作用仅限于本进程,,其他的进程无法获取该锁。

4.可重入

一个函数可重入,表示这个函数还没有执行结束,由于外部因素或内部调用,又一次进入该函数执行。
一个函数要被重入,只有两种情况:
<1> 多个线程同时执行该函数。
<2> 函数自身调用自身。

5.线程安全

1. 防止线程过度优化:volatile

volatile作用:
<1> 阻止编译器为了提高速度将一个变量缓存到寄存器内不写回。
<2> 阻止编译器调整操作volatile变量的顺序。

注意:volatile能够阻止编译器调整顺序,但是无法阻止CPU动态调度换序。

volatile T* pInst = NULL;
T* GetInstance(){
    
    
	if(pInst == NULL){
    
    	//实例未创建时才被构造
		lock();			//保证同时仅有一个线程进入实例构造
		if(pInst == NULL){
    
    	//A线程拿到了锁在构造实例,B线程进入了if正在等待锁,防止A释放锁后B拿到锁继续实例构造
			pInst = new T;
		}
		unclok();
	}	
	return pInst;
}

问题出现在步骤pInst = new T,实际包含了三个步骤:
<1> 分配内存。
<2> 在内存的位置上调用构造函数。
<3> 将内存的地址赋值给pInst。
这三步中,<2>和<3>的顺序可能被CPU颠倒。
可能引发的问题是:A线程将pInst已经赋值,但是对象构造尚未结束,此时C线程调用GetInstance(),判断第一个if时pInst != NULL成立,直接返回尚未构造完全的对象地址(pInst)以提供用户使用,可能导致程序崩溃。

扫描二维码关注公众号,回复: 12915932 查看本文章

2. 防止CPU调度换序:barrier

一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,barrier相当于一道拦水坝。

#define barrier() __asm__volatile ("lwsync")
volatile T* pInst = NULL;
T* GetInstance(){
    
    
	if(pInst == NULL){
    
    	//实例未创建时才被构造
		lock();			//保证同时仅有一个线程进入实例构造
		if(pInst == NULL){
    
    	//A线程拿到了锁在构造实例,B线程进入了if正在等待锁,防止A释放锁后B拿到锁继续实例构造
			T* temp = new T;
			barrier();	//防止CPU调换"T* temp = new T;"和"pInst = temp;"的顺序,保证pInst被赋值时一定是已经构造完毕的对象的地址
			pInst = temp;
		}
		unclok();
	}	
	return pInst;
}

6.线程模型

操作系统中的线程,分为内核线程和用户线程。

1.一对一模型

一个用户使用的线程唯一对应一个内核使用的线程,但是反过来不一定,即一个内核里的线程在用户态不一定有对应的线程存在。

优点:线程之间的并发是真正的并发,一个线程因为某原因阻塞时,其他线程的执行不会收到影响。

缺点:(1) 由于操作系统限制了内核线程数目,导致用户线程收到限制。
(2)内核线程切换的开销大,导致用户线程执行效率下降。

一般使用API或系统调用的线程均为一对一的线程。

2.多对一模型

将多个用户线程映射到一个内核线程,线程之间的切换由用户态的代码完成。

优点:线程切换速度快。

缺点:如果其中一个用户线程阻塞,导致对应内核线程阻塞,那么所有的线程都将无法执行。

3.多对多模型

将多个用户线程映射到多个(至少2个)内核线程上。

优点:(1) 一个用户线程的阻塞不会使得所有用户线程阻塞。
(2) 用户线程的数量没有限制。
(3) 在多核处理器上,可以提高性能,但是不如一对一模型高。

缺点:管理更复杂。

猜你喜欢

转载自blog.csdn.net/weixin_43202635/article/details/112134020