Python并发编程之谈谈线程中的“锁机制”(三)
大家好,并发编程 进入第三篇。
今天我们来讲讲,线程里的锁机制。
本文目录
何为Lock( 锁 )?如何使用Lock( 锁 )?为何要使用锁?可重入锁(RLock)防止死锁的加锁机制饱受争议的GIL(全局锁)
何为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( 锁 )?
来简单看下代码,学习如何加锁,获取钥匙,释放锁。
import threading
# 生成锁对象,全局唯一
lock = threading.Lock()
# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
lock.acquire()
# 释放锁,归回倘,其他人可以拿去用了
lock.release()
需要注意的是,lock.acquire() 和 lock.release()必须成对出现。否则就有可能造成死锁。
很多时候,我们虽然知道,他们必须成对出现,但是还是难免会有忘记的时候。
为了,规避这个问题。我推荐使用使用上下文管理器来加锁。
import threading
lock = threading.Lock()
with lock:
# 这里写自己的代码
pass
with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。
. 为何要使用锁?
你现在肯定还是一脸懵逼,这么麻烦,我不用锁不行吗?有的时候还真不行。
那么为了说明锁存在的意义。我们分别来看下,不用锁的情形有怎样的问题。
定义两个函数,分别在两个线程中执行。这两个函数 共用 一个变量 n 。
def job1():
global n
for i in range(10):
n+=1
print('job1',n)
def job2():
global n
for i in range(10):
n+=10
print('job2',n)
n=0
t1=threading.Thread(target=job1)
t2=threading.Thread(target=job2)
t1.start()
t2.start()
看代码貌似没什么问题,执行下看看输出
job1 1
job1 2
job1 job2 13
job2 23
job2 333
job1 34
job1 35
job2
job1 45 46
job2 56
job1 57
job2
job1 67
job2 68 78
job1 79
job2
job1 89
job2 90 100
job2 110
是不是很乱?完全不是我们预想的那样。
解释下这是为什么?因为两个线程共用一个全局变量,又由于两线程是交替执行的,当job1 执行三次 +1 操作时,job2就不管三七二十一 给n做了+10操作。两个线程之间,执行完全没有规矩,没有约束。所以会看到输出当然也很乱。
加了锁后,这个问题也就解决,来看看
def job1():
global n, lock
# 获取锁
lock.acquire()
for i in range(10):
n += 1
print('job1', n)
lock.release()
def job2():
global n, lock
# 获取锁
lock.acquire()
for i in range(10):
n += 10
print('job2', n)
lock.release()
n = 0
# 生成锁对象
lock = threading.Lock()
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()
由于job1的线程,率先拿到了锁,所以在for循环中,没有人有权限对n进行操作。当job1执行完毕释放锁后,job2这才拿到了锁,开始自己的for循环。
看看执行结果,真如我们预想的那样。
job1 1
job1 2
job1 3
job1 4
job1 5
job1 6
job1 7
job1 8
job1 9
job1 10
job2 20
job2 30
job2 40
job2 50
job2 60
job2 70
job2 80
job2 90
job2 100
job2 110
这里,你应该也知道了,加锁是为了对锁内资源(变量)进行锁定,避免其他线程篡改已被锁定的资源,以达到我们预期的效果。
为了避免大家忘记释放锁,后面的例子,我将都使用with上下文管理器来加锁。大家注意一下。
. 可重入锁(RLock)
有时候在同一个线程中,我们可能会多次请求同一资源(就是,获取同一锁钥匙),俗称锁嵌套。
如果还是按照常规的做法,会造成死锁的。比如,下面这段代码,你可以试着运行一下。会发现并没有输出结果。
import threading
def main():
n = 0
lock = threading.Lock()
with lock:
for i in range(10):
n += 1
with lock:
print(n)
t1 = threading.Thread(target=main)
t1.start()
是因为,第二次获取锁时,发现锁已经被同一线程的人拿走了。自己也就理所当然,拿不到锁,程序就卡住了。
那么如何解决这个问题呢。
threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。threading模块除了提供Lock锁之外,还提供了一种可重入锁RLock,专门来处理这个问题。
import threading
def main():
n = 0
# 生成可重入锁对象
lock = threading.RLock()
with lock:
for i in range(10):
n += 1
with lock:
print(n)
t1 = threading.Thread(target=main)
t1.start()
执行一下,发现已经有输出了。
1
2
3
4
5
6
7
8
9
10
需要注意的是,可重入锁,只在同一线程里,放松对锁钥匙的获取,其他与Lock并无二致。
. 防止死锁的加锁机制
在编写多线程程序时,可能无意中就会写了一个死锁。可以说,死锁的形式有多种多样,但是本质都是相同的,都是对资源不合理竞争的结果。
以本人的经验总结,死锁通常以下几种
- 同一线程,嵌套获取同把锁,造成死锁。
- 多个线程,不按顺序同时获取多个锁。造成死锁
对于第一种,上面已经说过了,使用可重入锁。
主要是第二种。可能你还没明白,是如何死锁的。
举个例子。
线程1,嵌套获取A,B两个锁,线程2,嵌套获取B,A两个锁。
由于两个线程是交替执行的,是有机会遇到线程1获取到锁A,而未获取到锁B,在同一时刻,线程2获取到锁B,而未获取到锁A。由于锁B已经被线程2获取了,所以线程1就卡在了获取锁B处,由于是嵌套锁,线程1未获取并释放B,是不能释放锁A的,这是导致线程2也获取不到锁A,也卡住了。两个线程,各执一锁,各不让步。造成死锁。
经过数学证明,只要两个(或多个)线程获取嵌套锁时,按照固定顺序就能保证程序不会进入死锁状态。
那么问题就转化成如何保证这些锁是按顺序的?
有两个办法
- 人工自觉,人工识别。
- 写一个辅助函数来对锁进行排序。
第一种,就不说了。
第二种,可以参考如下代码
import threading
from contextlib import contextmanager
# Thread-local state to stored information on locks already acquired
_local = threading.local()
@contextmanager
def acquire(*locks):
# Sort locks by object identifier
locks = sorted(locks, key=lambda x: id(x))
# Make sure lock order of previously acquired locks is not violated
acquired = getattr(_local,'acquired',[])
if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
raise RuntimeError('Lock Order Violation')
# Acquire all of the locks
acquired.extend(locks)
_local.acquired = acquired
try:
for lock in locks:
lock.acquire()
yield
finally:
# Release locks in reverse order of acquisition
for lock in reversed(locks):
lock.release()
del acquired[-len(locks):]
如何使用呢?
import threading
x_lock = threading.Lock()
y_lock = threading.Lock()
def thread_1():
while True:
with acquire(x_lock):
with acquire(y_lock):
print('Thread-1')
def thread_2():
while True:
with acquire(y_lock):
with acquire(x_lock):
print('Thread-2')
t1 = threading.Thread(target=thread_1)
t1.daemon = True
t1.start()
t2 = threading.Thread(target=thread_2)
t2.daemon = True
t2.start()
看到没有,表面上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
Python并发编程之谈谈线程中的“锁机制”(三)的更多相关文章
- Python并发编程05 /死锁现象、递归锁、信号量、GIL锁、计算密集型/IO密集型效率验证、进程池/线程池
Python并发编程05 /死锁现象.递归锁.信号量.GIL锁.计算密集型/IO密集型效率验证.进程池/线程池 目录 Python并发编程05 /死锁现象.递归锁.信号量.GIL锁.计算密集型/IO密 ...
- Python 并发编程(一)之线程
常用用法 t.is_alive() Python中线程会在一个单独的系统级别线程中执行(比如一个POSIX线程或者一个Windows线程)这些线程将由操作系统来全权管理.线程一旦启动,将独立执行直到目 ...
- Python之路【第十六篇】:Python并发编程|进程、线程
一.进程和线程 进程 假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作), 而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源. 是 ...
- python并发编程之多进程(二):互斥锁(同步锁)&进程其他属性&进程间通信(queue)&生产者消费者模型
一,互斥锁,同步锁 进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 竞争带来的结果就是错乱,如何控制,就是加锁处理 part1:多个进程共享同一打印终 ...
- Java并发编程学习:线程安全与锁优化
本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑 ...
- python 并发编程 多线程 死锁现象与递归锁
一 死锁现象 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等 ...
- python并发编程-进程池线程池-协程-I/O模型-04
目录 进程池线程池的使用***** 进程池/线程池的创建和提交回调 验证复用池子里的线程或进程 异步回调机制 通过闭包给回调函数添加额外参数(扩展) 协程*** 概念回顾(协程这里再理一下) 如何实现 ...
- python 并发编程 多线程 守护线程
做完工作这个进程就应该被销毁 单线程情况: 一个进程 ,默认有一个主线程 ,这个主线程执行完代码后 ,就应该自动销毁.然后进程也销毁. 多线程情况: 主线程代表进程结束 一个进程可以开多个线程,默认开 ...
- python 并发编程 多线程 开启线程的两种方式
一 threading模块介绍 multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性 二 开启线程的两种方式 第一种 每造一个进程,默认有一个线程,就是 ...
随机推荐
- zipkin
转:https://blog.csdn.net/liaokailin/article/details/52077620 zipkin为分布式链路调用监控系统,聚合各业务系统调用延迟数据,达到链路调用监 ...
- 《SpringMVC从入门到放肆》九、SpringMVC注解式开发(简单参数接收)
上一篇我们学习了注解式开发的配置方式并写了一个小Demo跑起来.今天我们来学习注解开发的参数接收.处理器方法中的常用参数有五类,这些参数会在系统调用时由系统自动赋值,即程序员可以在方法中直接使用.具体 ...
- (BUG记录)使用迭代器安全的删除处于循环下集合中的元素
今日在写一个功能时,需要从MQ拿取数据集合调用对端系统进行批量处理,为了幂等支持,在循环内部如果不满足调用条件就直接从集合中移除. 以上是一个典型的循环集合内删除的场景任务,工作一年第一次遇到这个场景 ...
- mysql 水平分表
新建10张表,user_0,user_1,...user_9,方法不可串用,采用hash或取余法,获取要操作的表名,取值用对应存值的方法 1.hash取余法 public function part_ ...
- 问题:AJAX的send参数里,空格以及它后面的数据在传递时消失(已解决)
今天,我在写关于前端向后台传递一些数字进行排序,并把结果返回到前端的程序.我用的是POST方式,须要在send里面写键值对. 前端和后台都写完,但是,前端传递给后台的数据是一部分,我检查了下,发现获取 ...
- angular.js学习笔记(一)
1.angular单项数据绑定 2.不要使用控制器的时候: 任何形式的DOM操作:控制器只应该包含业务逻辑.DOM操作则属于应用程序的表现层逻辑操作,向来以测试难度之高闻名于业界.把任何表现层的逻辑放 ...
- Redis安装完整步骤
安装: 1.获取redis资源 wget http://download.redis.io/releases/redis-4.0.8.tar.gz 2.解压 tar xzvf redis-4.0.8. ...
- Java 读取excel 文件流
public static void initType(String path) { try { //1 获取excel文件流 excel xls 文件 暂不支持xlsx if (path.conta ...
- Java synchronized和 Lock 的区别与用法
在分布式开发中,锁是线程控制的重要途径.Java为此也提供了2种锁机制,synchronized和lock.做为Java爱好者,自然少不了对比一下这2种机制,也能从中学到些分布式开发需要注意的地方. ...
- toastr操作完成提示框
toastr.js组件 关于信息提示框,项目中使用的是toastr.js这个组件,这个组件最大的好处就是异步.无阻塞,提示后可设置消失时间,并且可以将消息提示放到界面的各个地方. 官方文档以及源码 源 ...