我们线程的切换过程有两种:
第一种是线程在CPU上面的时间片到了,操作系统将该线程剥离下来
第二种是有优先级更高的进程,操作系统将优先级低的进程剥离下来,将优先级高的进程替换上去。
这个切换过程发生在内核返回用户态之时,因为一个进程的运行总是离不开系统调用的,当进入内核,在切换回用户态的时候,操作系统会检测是否有新的线程来了,并且进行优先级的比较,进而实现线程的切换
1.线程如果是通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值
2.如果线程被别的线程调用pthread_cancel终止掉,value_ptr所指向的单元里面存放的是常数 PTHREAD_CANCELED也就是-1,这是一个宏
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
4.如果对thread线程的终止状态不关心,可以传NULL给value_ptr参数
新线程退出时,必须被等待,已经退出的线程,它的空间不会被释放,仍然在进程的地址空间内,这样创建的线程不会复用刚才退出线程的地址空间,有点类似僵尸进程问题(由于线程的退出的情况复杂,和操作系统和pthread库是有关系的,很难演示出来,因此不做演示)
线程的等待方式全是阻塞的,没有非阻塞的
只终止某个线程而不终止整个进程的方法:
但是这种方法对主线程不适用,因为从main函数调用return,main执行完之后, 会调用exit(),exit() 会让整个进程终止,那所有线程自然都会退出。主线程结束,整个进程也会随之结束,因为进程是承担系统分配资源的基本实体,所有的线程都是基于这个资源进行分配的,当进程都不在后,自然要进行资源的回收
默认情况下,新创建的线程是需要被等待退出的,否则会无法释放资源,如果不关心线程的返回值,这时join是一种负担,这个时候我们可以告诉操作系统进,当线程退出时,自动释放线程资源
线程分离了,就好比几兄弟分家,一旦分家之后,父母就不会再管你要生活费,线程分离同样如此当分离之后,主线程就不会再关注新线程的情况,新线程的资源就独立了,线程退出之后,自己就释放了自己的资源,不再往自己的PCB之中写入退出码,主线程也不再需要进行等待
但是线程出现了错误,还是会影响其他的进程,因为就算他们进行了分离,本质还是属于同一个进程的
一次保证只有一个线程进入临界区,访问临界资源,就叫做互斥(也叫做互斥锁)
临界资源:被多个执行流同时访问的公共资源叫做临界资源
临界区:访问临界资源的代码叫做临界区
比如我们的线程都去访问同一个全局变量,这个全局变量就是临界资源。而我们的main函数之中的资源,其它的线程也能看到,但是不会去进行访问,因此不是临界资源
为了解决多个线程同时访问临界资源带来的问题,提出了锁的概念。linux之中将这把锁叫做互斥量
加锁的粒度越小越好,因为加锁的地方是串行的,加锁的地放越多,串行的地放也随之越多,对多线程的弱化作用也就越大
1.对临界区进行保护,所有的线程都必须遵守这个规则(规则保证)
2.加锁(lock)->访问临界区->解锁(unlock)
3.所有的线程必须看到同一把锁,因此锁本身也是临界资源。所以锁得先保证自身的安全,即申请锁的过程之中不能有中间状态也就是两态的,必须是原子性的,对应的解锁也是原子性的
4.在 加锁(lock)->访问临界区->解锁(unlock)过程之中,访问临界区是要花时间的,如果这时有其它线程过来申请锁,一定是申请不到的,那么其它线程该如何呢?答案是,操作系统会将新的线程的PCB投入对应的等待队列之中(将PCB的R状态改为S状态),解锁之后对线程进行唤醒操作(队列先进先出,优先唤醒队头的线程)
5.如何理解POSIX 之中的pthread库中的mutex?
一个程序的运行,有可能有许多线程,并且有许多临界资源,也会有很多把锁,因此锁也是需要被管理起来的,描述锁的结构体类似如下的结构
6.一次保证只有一个线程进入临界区,访问临界资源,就叫做互斥(也叫做互斥锁)
7.为什么加锁的效率一般比较低或者是影响效率?
因为加锁使线程由并发/并行的 ——>串行的,来带临界区没有申请到锁的线程只能进程阻塞状态,在等待队列之中等待被唤醒
死锁是指在同一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占有不会释放的资源而处于一种永久等待的转态
好比两个小孩去买玩具,玩具价格是10元,而每个小孩都拥有5元,小孩A向小孩B要钱,小孩B向小孩A要钱,但是两个小孩都不愿意给对方,于是陷入一种僵持转态
1.互斥条件:
一个资源每次只能被一个执行流使用
解决办法:不用锁
2.请求与保持条件:
一个执行流引请求资源而阻塞时,对方已获得的资源保持不放
解决办法:直接给对方(通过算法实现,比如谁优先级高给谁)
3.不剥夺条件:
一个执行流已获得的资源,在未使用完之前,不能强行剥离
解决办法:操作系统可以进行强行剥夺(比如优先级高剥夺优先级低的)
4.循环等待条件:
若干执行流之间形成一种头尾相接的循环等待资源的关系
解决办法:改变环路关系,变为顺序申请锁
5.一个进程也可能死锁
当一个进程申请锁,没有解锁,然后再次申请锁的时候,就被挂起了
破坏死锁的四个必要条件(随便破坏一个即可)
加锁顺序一致
避免锁未释放的场景
资源一次性分配(能不用锁就不用锁)
死锁检测算法
银行家算法
线程安全:
多个线程并发同一段代码时,出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题(多个线程,调用不可重入函数导致的)
可重入:
同一个函数,被不同的执行流调用,当前一个执行流还没执行完,就有其它的执行流再次进入,我们成之为重入。
一个函数在重入的情况下,运行结果不会出现任何问题,该函数被称为可重入函数,否则是不可重入函数(95%的函数是不可重入的)
常见线程不安全情况:
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,没有写入的权限,就不会改变共享资源,一般就是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见可重入的情况:
1.不使用全局变量或者静态变量
每个线程私有一个栈,而全局变量或者静态变量是共享的,因此不使用全局或者静态变量就能在一定程度上保证安全吸顶
2.不使用malloc或者new开辟空间
malloc和new的空间都是在堆上的,因此STL的容器基本上是不可重入的,因为它们都会自动进行扩容
3.不调用不可重入函数
4.不返回静态或者全局数据,所有数据都由函数的调用者提供
5.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
比如C语言提供的接口基本上都包括全局变量errno,在线程之中就将其拷贝了一份,保证每个线程是私有的
可重入与线程安全联系
函数是可重入的,那就是线程安全
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数一定是线程安全的
总结:
可重入函数:
一个函数函数可以被多个线程重入并且不出错,就叫做可重入函数
线程安全:
多个线程能独立就独立,尽量不要产生线程互相影响,一旦互相影响,就可能会出现线程不安全现象
STL和智能指针都不是线程安全的。STL极度追求效率、加锁会造成效率的影响
先举个例子:假如有两个线程,线程A负责往队列之中添加数据(队列为空就添加),B负责从队列之中读取数据(队列不为空)
如果线程A的优先级高于线程B,竞争力比B强, 假设1万次中,A连续成功的竞争申请到了9千次锁,但是只在第一次放入了数据,后面因为
队列不为空,因此只是重复的进行申请锁和释放锁的过程
这样虽然不会出错,成功的保护了临界资源,但是A做了很多没有意义的事情,这样效率就会非常低下,非常不合理
合理的方式是:
A申请锁,放数据,释放锁、当A条件不满足(队列不为空)后就不再进行申请了,直接通知B来申请
B再来申请锁,读数据,释放锁、当B线程的条件不满足后(队列为空),也不再继续了,然后通知A
这两种方式都没有错,但是第二种在保证安全的情况下,完成了临界资源和临界区的访问具有一定的顺序性,这就叫做同步
即:在保证数据安全的前提下(一般是加锁),让多个线程能够按照某种特定的顺序访问临界资源,从而有效的避免饥饿问题,这种就叫做同步
1.当线程访问临界资源,发现条件不满足->挂起等待,释放锁
2.发现条件不满足->通知对方
通过上述可知,多线程互斥访问临界资源时,容易造成效率低下的问题,这时候我们可以通过条件变量来控制线程是继续,还是挂起等待、通知其他进程
生产者消费者模型是生产者将产品放在交易场所,消费者将东西拿回去。
在计算机中本质是有一段内存空间,有多个线程进行生产,有多个线程进行消费