Linux线程(三)
一、互斥量
根据前面的分析,得到的结果不是我们想要的原因是–ticket操作不是原子操作,这个共享资源可能并发的切换大其他线程,导致有多个线程同时影响到这个共享资源,所以导致得到的结果不对。
1.解决方法(加锁—>Linux中叫这把锁为互斥量):
- 代码必须有互斥行为:当有一个执行流(有一个线程)进入临界区时,不允许其他线程进入该临界区
- 如果多个线程同时要求执行临界区的代码,并且临界区内没有线程在执行,那么只允许一个线程进入该临界区
- 如果线程不在临界区内执行,那么该线程不能阻止其他线程进入临界区
二、.互斥量的接口:
1.初始化互斥量
方法一:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
-
功能:初始化互斥量
-
参数:pthread_mutex_t *restrict mutex:要初始化的互斥量
const pthread_mutexattr_t *restrict attr:指定了新建互斥锁的属性。如果参数attr为NULL,则使用默认的互斥锁属性,默认属性为快速互斥锁 -
返回值:成功返回0,失败返回错误码
2.销毁互斥量:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 功能:是销毁一个互斥量
- 参数:要销毁的互斥量
- 注意⚠️:使用PTHREAD_MUTEX_INITIALIZER(静态初始化的互斥量)的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量(即销毁前要释放锁)
- 已经销毁的互斥量,要确保后面不会有线程去尝试对其加锁
3.对互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex)
调用pthread_mutex_lock可能会遇到以下几种情况
- 互斥量处于未锁状态,该函数将对该互斥量加锁,同时返回成功
- 发起调用pthread_mutex_lock函数时,其他线程已经把互斥量加完锁,或者存在其他线程同时申请对同一个互斥量进行加锁,但自己没有成功竞争到互斥量,那么pthread_mutex_lock调用就会陷入阻塞(即执行流被挂起),直到成功竞争到互斥量的线程释放锁,该执行流才可能解除阻塞
- 注意:互斥锁pthread_mutex也叫“挂起等待锁”,一旦线程获取锁失败,就会挂起到操作系统的一个等待队列中,这个获取锁失败而进入挂起等待队列的线程具体什么时候恢复执行也是不一定的,也不是获取到锁的线程释放锁后立即能执行的,因为线程的万恶之源”抢占式“。
4.改进前面的代码
#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);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
} i
nt 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);
}
从上面的结果看现在的代码已经是正确的了。
5.互斥锁的缺陷:
- 互斥锁虽然能够保证线程安全,但是最终导致程序的效率会受到影响,并且还有产生死锁的风险
- 产生死锁的场景:a.一个线程进行加锁后,再次尝试对同一个临界资源加锁 b.多个线程多把锁极易产生死锁
三、互斥量实现用原理探究
- 经过上面的例子,我们已经知道单纯的++ / – 操作都不是原子的,有可能会导致数据一致性问题
- 为了实现互斥锁操作,大多数体系都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据进行交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时也只能等待另一个处理器的交换指令总线周期执行结束。
四、可重入VS线程安全
可重入 | 线程安全 |
---|---|
重入是指同一个函数被不同的执行流调用时,前一个执行流的流程还未结束,就有其他的执行流再次进入函数。一个函数在从重的情况下,运行结果不会出现任何不同或者没有任何执行问题,则称该函数为可重入函数,反之为不可重入函数 | 线程安全是指多个线程并发执行同一段代码,不会出现不同的结果或不会出现执行问题。其中如果对全局变量或对静态变量进行操作,并且没有锁的保护的情况下,就有可能出现线程安全问题 |
注意:⚠️:可重入包含了线程安全问题。即如果一个函数可重入就一定线程安全,反之如果一个函数是线程安全的就不一定是可重入的
2.常见的线程不安全的情况:
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
3.常见的线程安全的情况:
- 每个线程对全局变量或静态变量只有读取的权限而没有写入的权限,一般来说这些线程是线程安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果出现二义性
4.常见的不可重入的情况:
- 调用了malloc/free函数,因为malloc是用全局链来管理堆的
- 调用了标准IO库函数,标准IO库的很多实现都以不可重入的方式使用全局数据结构的
- 可重入函数体内使用了静态的数据结构
5.常见可重入情况:
- 不使用全局变量或静态变量
- 不使用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有的数据都有函数的调用者提供
- 使用本地数据,或者使用通过制作全局数据的本地拷贝来保护全局数据
6.可重入与线程安全的联系:
- 函数是可重入的就是线程安全的
- 函数是不可重入的,有可能会有线程安全问题
- 如果一个函数内有全局变量/静态数据等,那么这个函数既不是线程安全的也不是可重入的
7.可重入与线程安全的区别:
- 可重入函数是线程安全函数的一种
- 线程安全不一定可重入,而可重入函数一定是线程安全函数
- 如果将对临界资源的访问加上锁,这这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
五、死锁
1.死锁:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因为互相申请被其他进程所占用的不会被释放的资源而处于一种永久等待的状态
2,产生死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持的条件:一个执行流因请求资源而阻塞时,要对已获得资源的进程进行请求保持不放
- 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干个执行流之间形成一种头尾相接的循环等待资源的关系
3.避免死锁
- 加锁的顺序要一致
- 避免锁未释放的场景
- 资源一次性分配
- 让临界区代码尽可能的短一些
- 让临界区代码不要调用复杂的函数
- 让临界区的代码尽量快的执行结束
4.避免死锁的算法
- 死锁检查算法
- 银行家算法
来源:CSDN
作者:wolf鬼刀
链接:https://blog.csdn.net/wolfGuiDao/article/details/103940053