python 线程锁_谈谈Python线程中的“锁机制”

原标题:谈谈Python线程中的“锁机制”

何为Lock( 锁 )?如何使用Lock( 锁 )?为何要使用锁?可重入锁(RLock)防止死锁的加锁机制饱受争议的GIL(全局锁)

何为Lock( 锁 )?

何为 Lock( 锁 ),在网上找了很久,也没有找到合适的定义。可能 锁 这个词已经足够直白了,不需要再解释了。

但是,对于新手来说,我还是要说下我的理解。

我自己想了个生活中例子来看下。

有一个奇葩的房东,他家里有两个房间想要出租。这个房东很抠门,家里有两个房间,但却只有一把锁,不想另外花钱是去买另一把锁,也不让租客自己加锁。这样租客只有,先租到的那个人才能分配到锁。X先生,率先租到了房子,并且拿到了锁。而后来者Y先生,由于锁已经已经被X取走了,自己拿不到锁,也不能自己加锁,Y就不愿意了。也就不租了,换作其他人也一样,没有人会租第二个房间,直到X先生退租,把锁还给房东,可以让其他房客来取。第二间房间才能租出去。

换句话说,就是房东同时只能出租一个房间,一但有人租了一个房间,拿走了唯一的锁,就没有人再在租另一间房了。

回到我们的线程中来,有两个线程A和B,A和B里的程序都加了同一个锁对象,当线程A率先执行到lock.acquire()(拿到全局唯一的锁后),线程B只能等到线程A释放锁lock.release()后(归还锁)才能运行lock.acquire()(拿到全局唯一的锁)并执行后面的代码。

这个例子,是不是让你清楚了什么是锁呢?

如何使用Lock( 锁 )?

来简单看下代码,学习如何加锁,获取钥匙,释放锁。

2a4c77af51e94c43b39f0804d88097ba.png

需要注意的是,lock.acquire() 和 lock.release()必须成对出现。否则就有可能造成死锁。

很多时候,我们虽然知道,他们必须成对出现,但是还是难免会有忘记的时候。

为了,规避这个问题。我推荐使用使用上下文管理器来加锁。

922ac27da9534fadbded6f00da7a88e9.png

with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。

为何要使用锁?

你现在肯定还是一脸懵逼,这么麻烦,我不用锁不行吗?有的时候还真不行。

那么为了说明锁存在的意义。我们分别来看下,不用锁的情形有怎样的问题。

定义两个函数,分别在两个线程中执行。这两个函数 共用 一个变量 n 。

35f3ac8f869e435d832d5ffb93f5fb6c.png

看代码貌似没什么问题,执行下看看输出

c7e5bcc870d94673baf5fb87d9f4bbfe.png

是不是很乱?完全不是我们预想的那样。

解释下这是为什么?因为两个线程共用一个全局变量,又由于两线程是交替执行的,当job1 执行三次 +1 操作时,job2就不管三七二十一 给n做了+10操作。两个线程之间,执行完全没有规矩,没有约束。所以会看到输出当然也很乱。

加了锁后,这个问题也就解决,来看看

179c9d3c700545bf83750247fa5e633c.png

由于job1的线程,率先拿到了锁,所以在for循环中,没有人有权限对n进行操作。当job1执行完毕释放锁后,job2这才拿到了锁,开始自己的for循环。

看看执行结果,真如我们预想的那样。

a4c6fd2f88804e578c9d614f711686ea.png

这里,你应该也知道了,加锁是为了对锁内资源(变量)进行锁定,避免其他线程篡改已被锁定的资源,以达到我们预期的效果。

为了避免大家忘记释放锁,后面的例子,我将都使用with上下文管理器来加锁。大家注意一下。

可重入锁(RLock)

有时候在同一个线程中,我们可能会多次请求同一资源(就是,获取同一锁钥匙),俗称锁嵌套。

如果还是按照常规的做法,会造成死锁的。比如,下面这段代码,你可以试着运行一下。会发现并没有输出结果。

3622772b6d22435da1d7da0990857aa0.png

是因为,第二次获取锁时,发现锁已经被同一线程的人拿走了。自己也就理所当然,拿不到锁,程序就卡住了。

那么如何解决这个问题呢。

threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。

threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。

2482e2786fcc497f8a71e674fa4663f5.png

执行一下,发现已经有输出了。

c5853a1230d84779b7509d7e0be39b03.png

需要注意的是,可重入锁,只在同一线程里,放松对锁钥匙的获取,其他与Lock并无二致。

防止死锁的加锁机制

在编写多线程程序时,可能无意中就会写了一个死锁。可以说,死锁的形式有多种多样,但是本质都是相同的,都是对资源不合理竞争的结果。

以本人的经验总结,死锁通常以下几种

同一线程,嵌套获取同把锁,造成死锁。

多个线程,不按顺序同时获取多个锁。造成死锁

对于第一种,上面已经说过了,使用可重入锁。

主要是第二种。可能你还没明白,是如何死锁的。

举个例子。

线程1,嵌套获取A,B两个锁,线程2,嵌套获取B,A两个锁。

由于两个线程是交替执行的,是有机会遇到线程1获取到锁A,而未获取到锁B,在同一时刻,线程2获取到锁B,而未获取到锁A。由于锁B已经被线程2获取了,所以线程1就卡在了获取锁B处,由于是嵌套锁,线程1未获取并释放B,是不能释放锁A的,这是导致线程2也获取不到锁A,也卡住了。两个线程,各执一锁,各不让步。造成死锁。

经过数学证明,只要两个(或多个)线程获取嵌套锁时,按照固定顺序就能保证程序不会进入死锁状态。

那么问题就转化成如何保证这些锁是按顺序的?

有两个办法

人工自觉,人工识别。

写一个辅助函数来对锁进行排序。

第一种,就不说了。

第二种,可以参考如下代码

4017221ced40490b95a3689de51ed75d.png

如何使用呢?

0e47e8e2c4df4c1693f8638024a947ee.png

看到没有,表面上thread_1的先获取锁x,再获取锁y,而thread_2是先获取锁y,再获取x。

但是实际上,acquire函数,已经对x,y两个锁进行了排序。所以thread_1,hread_2都是以同一顺序来获取锁的,是不是造成死锁的。

饱受争议的GIL(全局锁)

在第一章的时候,我就和大家介绍到,多线程和多进程是不一样的。

多进程是真正的并行,而多线程是伪并行,实际上他只是交替执行。

是什么导致多线程,只能交替执行呢?是一个叫GIL(Global Interpreter Lock,全局解释器锁)的东西。

什么是GIL呢?

任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

需要注意的是,GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。而Python解释器,并不是只有CPython,除它之外,还有PyPy,Psyco,JPython,IronPython等。

在绝大多数情况下,我们通常都认为 Python == CPython,所以也就默许了Python具有GIL锁这个事。

都知道GIL影响性能,那么如何避免受到GIL的影响?

使用多进程代替多线程。

更换Python解释器,不使用CPython返回搜狐,查看更多

责任编辑:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值