线程安全=同步+互斥

线程安全:多个线程同时对临界资源(例如,全局变量)进行访问而不会造成数据二义。(临界区:对临界资源操作的代码,说白了临界区就是对临界资源的操作。)
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
如何实现线程安全?
同步+互斥
同步:对临界资源访问的时序合理性,是保证合理的。
互斥:对临界资源同一时间访问的惟一性,是保证安全的。(任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。)
在这里插入图片描述
在这里插入图片描述
if语句判断条件为真以后,代码可以并发的切换到其他线程。
usleep模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
ticket–操作本身就不是一个原子操作,下面是ticket—的汇编代码。
在这里插入图片描述
ticket–操作并不是原子操作,而是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update: 更新寄存器里面的值,执行-1操作。
store:将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:
1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3、如果线程不在临界区中执行,那么该线程不能阻止其它线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量/锁
在这里插入图片描述
线程间互斥的实现:互斥锁/量,说白了就是一个只有0或1原子操作(原子操作:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成)的计数器+等待队列,标记当前临界资源是否能进行操作。
这个计数器也是个临界资源,那如何保证计数器的安全?
得保证计数器的修改得是原子操作,我们对一个数据进行修改的时候,先把数据从内存加载到cpu寄存器上,修改,然后再放回内存。现在将一个寄存器的值修改成0,直接一步拿寄存器的值和内存计数器的值进行数据交换。
1、定义互斥锁变量pthread_mutex_t;//该变量是全局的或者传参
2、对互斥锁变量进行初始化 pthread_mutex_init(&mutex, &attr);//参数2是设置互斥锁的属性,通常没什么可设置的,给NULL。
还有一种就是在定义互斥锁变量的时候直接初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MTEX_INITIALIZER这个宏就是结构体赋值。
但常用的是接口初始化。
注意:无论是1还是2,初始化一定要放在线程创建之前。
3、在对临界资源操作之前先加锁,也就是对临界区进行加锁保护。
pthread_mutex_lock(&mutex);//阻塞的,若可以加锁,就是把1改成0,直接修改计数,函数返回;否则挂起等待。
pthread_mutex_trylock(&mutex);//非阻塞加锁,先尝试一下是否能加锁,能加的话就返回正确,不能加的话,返回错误码。和循环一起使用
pthread_mutex_timedlock(&mutex);//先尝试一下是否能加锁,不能加的话就等一会,时间超时的话,返回错误码。
该函数有一个使用的前提,#define __USE_XOPEN2K,不定义这个宏就用不了这个函数,也就是说默认这个函数是不被使用的。
4、对临界资源操作完毕后进行解锁
pthread_mutex_unlock(&mutex);//就是把0改成1,并唤醒等待。
注意:加锁只能加一次,否则会陷入等待,而解锁可以解多次。
5、销毁互斥锁 pthread_mutex_destroy(&mutex);
销毁互斥量需要注意:
1、使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
2、不要销毁一个已经加锁的互斥量
3、已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
代码

死锁

多个线程同时对锁资源进行竞争访问,但是因为加锁和解锁的争抢推进顺序不当,导致多个线程相互僵持,使线程无法继续往下运行。

死锁产生的四个条件

1、互斥条件:我拿到锁,你就不能拿了。
2、不可剥夺条件:我加的锁,你是可以通过unlock解开的,但是你不能这么做,因为这样的话,锁的存在也有没有任何意义了。
互斥锁 = 1+2
3、请求与保持条件(阻塞加锁): 我拿了A锁之后,去请求B锁,拿不到B锁的话,A锁也不放手。
注意:一个锁是不可能产生死锁的,只有在同时使用多个锁的情况下才会产生死锁。
4、环路等待条件:我拿了A锁之后,去请求B锁,对方拿了B锁之后,去请求A锁,双方请求不到还都不放手。

怎么预防?

其实4个条件只要破坏其中的一个就可以了。破坏第3个条件是使用非阻塞/限时加锁,B锁不能请求到,那就不请求了,接着把自己的A锁解开给别人使用。
但是前两个条件是不可避免的,因为是锁的特性,我们只能避免环路等待。

死锁避免

死锁检测算法
银行家算法(很典型)

同步

线程间同步的实现:等待+唤醒
当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。
例如,一个线程处理数据,首先你得有数据,其他线程产生数据之后你才能处理数据。
操作条件不满足则等待,别人促使条件满足后唤醒等待。

条件变量实现同步,互斥锁+等待+唤醒+等待队列只提供等待与唤醒的操作,并不提供条件的判断,这是用户要做的事。
1、线程在对临界资源访问前,先判断能否进行操作;若可以操作,则线程直接操作,否则条件变量提供等待的功能,让pcb等待在队列上。
2、其他线程促使操作条件满足,然后唤醒条件变量的等待队列上的线程。

1、定义条件变量pthread_cond_t cond;
2、条件变量初始化 pthread_cond_init(&cond, NULL);//通常置空
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//这种方式的初始化可以不用销毁,不怎么常用。
3、用户在判断条件不满足的情况下提供等待功能
pthread_cond_wait(&cond, &mutex);
//此时线程就挂起了,就是将这个线程的pcb状态修改为可中断休眠状态,并将pcb加入到cond结构体内部所保存的等待队列当中。
pthread_cond_timedwait();//限时等待,等待超时后返回错误码。
pthread_cond_wait等价于pthread_mutex_unlock(&mutex);pause();pthread_mutex_lock(&mutex);这三步操作,其中,解锁和休眠是原子操作。

为什么条件变量要和互斥锁一起使用?

线程什么时候等待,需要一个判断条件,而这个条件也是一个临界资源(等待了之后,其它线程需要促使这个条件满足(修改这个临界资源)),因此这个临界资源的操作就需要受保护,默认使用互斥锁实现保护。
4、其他线程在促使条件满足后,唤醒等待。
pthread_cond_signal(&cond);//至少唤醒一个
pthread_cond_broadcast(&cond);//唤醒所有等待
在没有等待的情况下,使用pthread_cond_signal也是可行的,而且该函数还是正确返回0。
测试这个是为了解决“至少唤醒一个”的问题。
5、销毁条件变量 pthread_cond_destroy(&cond);
代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值