线程创建的时候,会重新分配PCB,但是页目录以及页表是共享的。
Linux中实际上没有线程,而是用进程来模拟线程。
进程是所有PCB组成的一个线程组。
Linux中的线程是轻量级进程。
协程是轻量级线程,完全由程序控制,也就是在用户态执行。
进程是具有一个或者多个执行流的线程组。
线程
一个进程至少有一个执行线程。
线程的执行粒度是比较细致的。(只执行进程的一部分内容)
线程是在进程的虚拟内存空间中运行的。
进程中的多个线程平分进程栈的大小。
为什么内核占1G,其余占3G?
这是缺省的,实际上在内核优化时可以更改。
多线程与多进程的对比
1.系统开销
多线程内部情况
一对一模型
缺点:1.用户的线程数量受到限制;
2.线程调度时,上下文切换开销大,导致用户线程的执行效率下降。
多对一模型
缺点:当某个用户线程阻塞,那么所有的线程都将无法执行。
优点:高效的上下文切换和无限制的用户线程数量。
多对多模型:
将多个用户线程映射到少数但不止一个内核线程上。
线程拥有自己的一部分数据:(线程的访问权限)
线程标识符
上下文数据(PC寄存器,程序计数器,栈指针) 面试重点 (线程切换,需要保护现场数据)
自己的调用栈(私有栈) 面试重点 (为了避免临时变量相互之间不被干扰) 并非完全无法被其他线程访问。
errno
信号屏蔽字
调度优先级
线程的局部存储(TLS) 某些OS为线程单独提供的私有空间,通常是很有限的容量。
一个进程的多个线程共享
同一地址空间(代码段,未初始化和初始化的数据段) 环境变量 以及 堆 、运行参数 文字代码区 函数 全局变量 静态变量
文件描述符表
每种信号的处理方式
当前工作目录
uid 和 gid
线程和进程的区别
进程是OS分配资源的基本单位,线程是CPU调度的基本单位。
线程创建和销毁的成本相较于进程,更低。(线程只需要销毁PCB即可,而进程需要页表,PCB,磁盘程序加载至物理内存等等)
不仅仅是体现在成本上,实际在时间上也比较低。
线程的调度成本更低。(线程切换时,并不需要切换页表,这是因为他们同属于一个进程,共享同一块虚拟内存空间。)
线程之间的关系亲密(线程之间共享资源),而进程之间是相对独立的(进程之间独占资源)。
线程与线程之间共享的数据包括
I/O状态信息,代码段,数据区,堆,初始化和未初始化全局变量
记账信息,寄存器信息,栈,程序计数器,线程的信号屏蔽码,线程的优先级,错误返回码(errno),线程ID是私有的。
使用多线程而不使用单线程的原因?
1.某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行,而多线程执行可以有效利用等待的时间。(等待网络响应)
2.科学计算会消耗大量的时间,如果是多线程的话,一个线程负责交互,一个线程负责计算。
3.程序逻辑本身需要并发操作,例如多端下载软件(Bittorrent)。
4.单线程程序无法全面地发挥计算机的全部计算能力。
5.相对于多进程应用,多线程在数据共享方面效率要高很多。
线程的优点
创建一个新线程的代价比创建新进程小得多
占用资源少
线程之间的切换需要OS做的少很多
能够利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务(网络I/O,磁盘I/O)
计算密集型应用(科学计算),为了能在多处理器系统上运行,将计算分解到多个线程去实现
I/O密集型应用(读写网络I/O),为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作(减少平均等待时间)
线程间的通信方便,因为共享了很多数据。
I/O密集型线程总是比CPU密集型线程更容易得到优先级的提升。
线程缺点
性能损失 启动线程过多时,CPU不够用,线程之间的切换次数就会变多,导致效率变低。
缺乏访问控制 eg: exit退出进程,而这些线程是隶属于同一个进程的,这样就会直接导致线程退出,不可控。
健壮性降低 一个线程在操作全局变量或者静态变量,某一个线程也在操作,
这可能会导致不良影响,换句话说,线程之间是缺乏保护的。(volatile)
编程难度提高 考虑的问题变多
线程调度
不断地在处理器上切换不同的线程的行为就叫做线程调度。
优先级调度
在优先级调度的环境下,线程的优先级改变一般有三种方式:
1.用户指定优先级;
2.根据进入等待状态的频繁程度提升或者降低优先级;
3.长时间得不到执行被提升优先级。(防止饿死)
抢占
线程在用尽时间片之后会被强制剥夺继续执行的权利,而进入就绪状态,这个过程叫做抢占。
线程创建
是有一个入口函数的。
头文件 #include <pthread.h>
pthread_t 是一个获取线程ID的一个类型 /usr/include/pthread.h /usr/include/bits/pthreadtypes.h
unsigned long int
库函数int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
1.线程标识符 tid(赋值型参数);2.线程属性参数(线程的继承,线程的堆栈大小,线程的保护区,线程的分离与等待),一般写NULL,默认属性;
3.线程启动后执行的函数,线程函数指针,由4的参数传入(函数指针是个地址);4.传给线程启动函数的参数,即就是传入3的一个参数。
关于第二个参数设置属性:
关于参数3的返回值类型void * : 通知别人执行结束(正常退出还是异常退出)
返回值:成功返回0,失败返回错误码。 读取返回值比读取线程内的errno变量的开销更小。
返回错误码是因为多线程中的函数运行的时候,产生错误可能是同时的,那么就很有可能执行某一个线程产生错误,而另一个线程产生的
错误会修改之前的线程的错误码,这样是不允许的。
如何看到两个执行流
pthread_create()函数的第三个参数就是一个函数指针,那么一个执行流执行下面的内容(主线程),
一个执行流去执行函数指针指向的函数中的内容(),这就是一个多线程。
编译链接时加选项-pthread或者-lpthread,需要注意的是在Ubuntu系统中加-pthread选项会报错。
报错
gcc create.c -o create
/tmp/ccyAx6Ef.o: In function `main':
create.c:(.text+0x7a): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
Makefile:4: recipe for target 'create' failed
make: *** [create] Error 1
实际上是没有链接相关的库,这里需要加上选项-lpthread。
ps -aL 可以显示轻量级进程(线程) 主线程id和进程id是一样的
打印出来的tid仅仅表示在当前进程中的线程,每个线程私有地址区域的地址。
ps -efL 查看线程信息(轻量级进程)
物理内存是一个空间,而虚拟地址相当于是一根线,这根线上的某个点表示一个编号,两点之间的距离就表示指向的某个区域大小。
页表可以理解为一个解引用操作,访问控制。
主线程:进程一开始创建的线程(默认有一个执行流) 线程没有父子关系,是一种对等的关系。
主线程在主线程栈,其他线程在别的栈区(有自己的范围)。
主线程id=进程id=线程组id
进程id(pid)实际上指的是线程组id(tgid)
tgid是线程组id 等于 主线程id(pid) (也就是ps 查看到的pid)
tid是用户层面可以看到的线程id(一个比较大的数字,用%lu打印) 用%p打印是一串地址,是个编号。
指向共享存储映射区中的私有地址信息,这个私有栈是在进程的虚拟存储空间内部的。
用户层面看到的是tgid
通过tid转换成私有的地址信息就可以找到对应的线程。
而LWP表示的是全局的线程ID(轻量级进程id,是在内核态的) tid是在用户层面表示的内核态中的轻量级进程id
Light Weighted Process
NLWP:线程组内线程的个数.
进程描述符结构体中的pid,表面上看起来是进程id,实际上是线程id。 pid_t pid;
进程描述符结构体中的tgid,该值对应的是用户层面的进程id。 pid_t tgid;
主线程的栈是在PCB的栈区,其他线程的栈是在共享存储映射区(存储线程的虚拟地址)。 区域仅仅只是代表了一个范围(start_code, end_code)
// 线程退出 (谁调用它它就退出哪个线程)
void pthread_exit(void *retval);
参数为线程退出的返回值 retval不要指向一个局部变量。
主线程pthread_exit(NULL),那么主线程退出,产生两个僵尸线程。
其他线程return 或者 pthread_exit(NULL),那么只是该线程不执行了。
主线程return,主线程退出,如果在main函数中,那么进程退出。
// 取消一个执行中的线程(需要知道其他线程的线程id) 异常退出
int pthread_cancel(pthread_t thread); // 成功返回0,失败返回错误码。
//获取线程标识符 不会出错
pthread_t pthread_self(void); //谁调用它返回谁的tid(用户层面的tid)
// 返回值与线程创建函数的第一个参数的返回值一样,是栈上的地址。
#include <sys/syscall.h>
// 获取线程标识符,这个值是和ps -efL 看到的一样。
pid_t gettid(void);
syscall(2); // 2表示内核中的一个系统调用的编号
syscall(SYS_gettid);
为什么需要线程等待?
1.已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
2.创建新的线程不会复用刚才退出线程的地址空间。
默认线程是等待状态,要不然没人收尸。
// 阻塞式等待 (获取退出状态)
// 成功返回0 失败返回错误码
// 功能:等待线程结束。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得
到的终止状态是不同的。
int pthread_join(pthread_t thread, void **retval);
// eg: void *ptr = NULL;
pthread_join(tid, &ptr);
1.如果thread线程通过return返回,retval指向的单元里存放的是thread线程函数的返回值;
2.如果thread线程被pthread_cancel异常终止,retval指向的单元里存放的是-1 (PTHREAD_CANCELED);
3.如果thread线程是自己调用pthread_exit终止,retval指向的单元里存放的是传给pthread_exit的参数。
// 分离线程 (可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离)
// 设置分离属性,指定线程退出后自动释放资源 不等待(不关心退出状态) 分离与等待属性是冲突的,
// 二者只能存在一个。就是说分离后就不能再等待了。
// 返回值:成功返回0 失败返回错误码
int pthread_detach(pthread_t thread); // 在创建线程时,第二个参数可以传入线程分离属性,但是太麻烦,使用库函数简化操作。
int pthread_attr_init(pthread_attr_t *attr); // 线程属性初始化
int pthread_attr_destory(pthread_attr_t *attr); // 线程属性销毁
定义一个pthread_attr_t变量
Thread attributes:
Detach state = PTHREAD_CREATE_JOINABLE // 分离属性
Scope = PTHREAD_SCOPE_SYSTEM // 系统范围内的抢夺CPU资源
Inherit scheduler = PTHREAD_INHERIT_SCHED // 继承调度策略
Scheduling policy = SCHED_OTHER // 分时调度
Scheduling priority = 0 // 优先级
Guard size = 4096 bytes // 线程栈末尾的警戒缓冲区大小
Stack address = 0x40196000 // 线程栈地址
Stack size = 0x201000 bytes // 线程栈大小(线程栈之间是有安全距离的,不是紧挨的,防止栈溢出影响下一个线程)
同步:协作的时序性(一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。)
互斥:具有排他性
临界资源:具有排他性(只能一个人用)的资源
临界区:进程中访问临界资源的一段需要互斥执行的代码,其作用范围仅限于本进程。
临界区的访问规则
空闲则入 忙则等待 有限等待 让权等待(可选)
线程安全
单条指令的操作是原子的,它的执行是不会被打断的。
可重入
一个函数被重入,表示这个函数没有执行完成,由于外部因素或者内部调用,又一次进入该函数执行。
一个函数要被重入,有两种方式:
1.多个线程同时执行该函数;
2.函数递归。
volatile
1.阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;
2.阻止编译器调整操作volatile变量的指令顺序。
--操作并不是原子操作,而是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update: 更新寄存器里面的值,执行-1操作
store:将新值,从寄存器写回共享变量ticket的内存地址
pthread_mutex_t *mutex // 互斥量
互斥量要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁。而信号量可以在一个线程获取之后由另一个线程释放。
访问临界资源时使用互斥量。
1.初始化互斥量;2.互斥量加锁;3.临界资源的操作;4.解锁。
互斥量的库:/usr/include/pthread.h
// 初始化锁(需要销毁)
pthread_mutex_init(&mutex, NULL); // 第一个参数是互斥量,第二个是互斥量属性, 互斥量是个全局的 或者直接赋值(不需要销毁)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配互斥量,不需要销毁
注意:
1.不要销毁一个已经加锁的互斥量;
2.已经销毁的互斥量,要确保后面不会有线程再次尝试加锁。
// 成功返回0,失败返回错误码。
pthread_mutex_trylock() //非阻塞 尝试加锁,如果不能加锁立即报错,获取不到锁资源 如果可以加锁立即返回0
pthread_mutex_timelock() //有时间限制的锁(挂起等待有时间限制)
pthread_mutex_lock(&mutex) //加锁
临界资源的操作(临界区,只允许一个线程执行,不允许多个线程同时执行)
pthread_mutex_unlock(&mutex) //解锁
// 销毁
pthread_mutex_destory(&mutex);
重点:加锁之后,要考虑到任意一个线程可能退出的状态,这时候就要去解锁,否则就有可能出现死锁。
信号量实际上是个计数器,表示可用资源的数目。
二元信号量实际上就是个互斥锁,它有两种状态:占用和非占用。它适合被单线程独占访问。
普通线程退出时不显示僵尸状态信息。
!ps 历史命令中最接近ps的一次命令。
产生死锁的四个必要条件:(全都满足)
1.互斥条件;
2.不可剥夺条件;(只能自己释放)
3.请求与保持条件;(假设两个锁A和B,那么获取到A锁后,但是无法获取B锁,这时候并不会释放掉A锁,一直等待)
4.环路等待条件。(双方互相请求对方的资源,但是不去释放自己的资源,重点是环形等待) 哲学家就餐
避免死锁:银行家算法(消除隐患:安全和不安全,相当于判断系统可分配资源数目和当前想要分配资源数目)
生产者-消费者问题(生活中的竞争问题)
生产者 -> 缓冲区 -> 消费者
有界缓冲区的生产者-消费者问题描述
一个或者多个生产者在生成数据后放在一个缓冲区中,一个消费者从缓冲区中取出数据处理,
任何时刻只能有一个生产者或者消费者可以访问缓冲区。
限制条件:
任何时刻只能有一个线程可以操作缓冲区(互斥访问)
缓冲区空时,消费者必须等待生产者(条件同步)
缓冲区满时,生产者必须等待消费者(条件同步)
两个角色:生产者和消费者
三种关系:生<->生:互斥 生<->消:互斥+同步 消<->消:互斥
一个场所:交易场所,也就是缓冲区。
生产者生产出来商品后,发送信号通知,但是这时候可能通知其他生产者,也可能通知消费者,
其中如果通知生产者时,由于这个时候缓冲区中有商品,那么生产者就会陷入等待,消费者没有将商品取出去,因此一直卡在那。
综上,应该发送信号通知给消费者,这样一个放,一个取,并且都会发送信号通知,这样就实现了生产者<->消费者模型。
为什么等待的时候要加入互斥量?
条件变量是个全局的,通过互斥量来约束它。wait函数本身对mutex进行了一次原子操作。 (原子操作:解锁和休眠)
为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所
以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。
没有互斥锁就无法安全的获取和修改共享数据。
条件变量
作为一种同步手段,作用类似于一个栅栏。线程可以有两种操作:等待和唤醒。
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
// 等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
eg: 条件等待
pthread_mutex_lock(&mutex);
while (条件为假) // 因为pthread_cond_wait有可能被信号打断而唤醒
pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);
eg: 给条件发送信号
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(&cond); // 如果没有线程等待,信号被丢弃。
pthread_mutex_unlock(&mutex);
// 唤醒单个等待
int pthread_cond_signal(pthread_cond_t *cond);
// 抢占式唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
信号量是一个包含等待队列的计数器。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突地访问共享资源的目的,但POSIX信号量用于线程间同步,其创建
的信号量只有1个,而SystemV信号量是创建一个信号量集。
POSIX信号量还创建了一块共享内存。
链接时加上-lrt 或者-lpthread
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem信号量
pshared为0表示线程间共享,非0表示进程间共享。
value是信号量初始值
返回值:成功返回0,失败返回-1
// 销毁信号量
int sem_destroy(sem_t *sem);
// 功能:等待信号量,会将信号量的值减1,相当于P操作。
int sem_wait(sem_t *sem);
// 功能:发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加1,相当于V操作。
int sem_post(sem_t *sem);
读写锁
读写锁有两种获取方式:共享的或者独占的。
应用场景:大量读,少量写
读读共享,都写互斥,写优先级高。
注意:写独占(其他写和读都是互斥),读共享,写锁优先级高。
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,
它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码
段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,
那就是读写锁。读写锁本质上是一种自旋锁。
自旋锁:不停地询问是否有锁。 (短时间处理资源)
读者写者
指的是我们对共享数据的访问,一类是读者,只是读取数据不会去修改,一类是写者,可能会读和修改,
读的时候,可以通过线程同时读,但是写的时候不能同时写。也就说在同一时刻允许多个读者同时进行读操作,
第二条是说读写互斥,如果有写者正在写,那这时候你读者去读是没有意义的。第三个呢 是写写互斥,同时往里写数据,
这也是不允许的。
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 取消读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 销毁读写锁
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);