多线程中锁那些事儿

我们就从一个生活例子入手。
你长大了,但是因为没有在大学好好学习的缘故,你入职了一家偏僻的小公司,办公环境还说的过去,但是至于厕所就只有一个了,更惨的事情是厕所里只有一个马桶,你们公司可以比作同一个进程空间,每个员工就是进程中不同的线程,干着各自的活,洗手间就是临界区,马桶就是共享变量(临界资源),当有一个人进厕所后就必须上锁,其他人如果也想上厕所首先就会看厕所的锁有没有上,如果上了,他就不会继续去访问厕所,如果厕所没锁,或者里面的人忘记了上锁,你应该想像那画面,有内味儿。。。。。
所以这个故事告诉我们要好好学习,找个拥有大厕所的公司。
上面纯属打趣儿。
编程中锁的重要性远非上面的例子那么一点点,但是和生活中加锁的目的是一样的,锁是保证代码安全性的重要因素。

下面进入正题。

1, 线程间通信

线程间通信的方式:

方式 区别
1、临界区 通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
2、互斥量 (互斥锁)Synchronized/Lock 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
3、信号量 Semphare 为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
4、事件(信号),Wait/Notify 通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

2, Linux的4种锁机制

种类 介绍
互斥锁:mutex 用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:rwlock 分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock 在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
RCU:即read-copy-update 在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

互斥锁和读写锁的区别

  • 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
    另外,继续上面的例子,如果有人想去洗手间时发现门锁上了,他也有两种策略:1,在洗手间那里等(阻塞); 2,暂时先离开等会再过来看(非阻塞);这就是阻塞锁和非阻塞锁。
  • 读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

3, 死锁

假设有A、B、C3个人在一起吃饭,每个人左右各有一只筷子。所以,这其中要是有一个人想吃鱼,他必须首先拿起左边的筷子,再拿起右边的筷子。现在,我们让所有的人同时开始吃饭。那么就很有可能出现这种情况。每个人都拿起了左边的筷子,或者每个人都拿起了右边的筷子,为了吃饭,他们现在都在等另外一只筷子。此时每个人都想吃饭,同时每个人都不想放弃自己已经得到的一那只筷子。所以,事实上大家都吃不了饭。就造成了死锁。这里筷子就是共享资源,三个人就是三个线程。

在这里插入图片描述

3.1 死锁产生的4个必要条件

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。死锁发生的四个必要条件如下:

四个必要条件 介绍
互斥条件 进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
请求和保持条件 (占有且等待) 进程获得一定的资源后,但是还有资源未得到满足,正在请求其他进程释放该资源。此时请求阻塞,但该进程不会释放自己已经占有的资源
不可剥夺条件 (不可抢占) 进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放 换个方式说,就是别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
环路等待条件(循环等待) 进程发生死锁后,必然存在一个进程-资源之间的环形链, 使得每个进程都占有下一个进程所需的至少一种资源。
  • 当以上四个条件均满足,必然会造成死锁,发生死锁的进程无法进行下去,它们所持有的资源也无法释放。这样会导致CPU 的吞吐量下降。所以死锁情况是会浪费系统资源和影响计算机的使用性能的。那么,解决死锁问题就是相当有必要的了。

3.2 死锁解决

产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。

  • 破坏“占有且等待”条件

方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。

扫描二维码关注公众号,回复: 9999777 查看本文章
  • 优点:简单易实施且安全。
  • 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成 资源浪费。使进程经常发生饥饿现象。

方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

  • 破坏“不可抢占”条件

当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用 的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。

  • 该种方法实现起来比较复杂,且代价也比 较大。释放已经保持的资源很有可能会导致进程之前的工作失效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这 不仅会延长进程的周转周期,还会影响系统的吞吐量。
  • 破坏“循环等待”条件

可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只 能申请编号大于i的资源。

总结

(1)死锁的危险始终存在,但是我们应该尽量减少这种危害存在的范围
(2)解决死锁花费的代价是异常高昂的
(3)最好的死锁处理方法就是在编写程序的时候尽可能检测到死锁
(4)多线程是一把双刃剑,有了效率的提高当然就有死锁的危险
(5)某些程序的死锁是可以容忍的,大不了重启机器,但是有些程序不行
发布了22 篇原创文章 · 获赞 58 · 访问量 7982

猜你喜欢

转载自blog.csdn.net/weixin_46027505/article/details/104991860