一篇文章理清Python多线程同步锁,死锁和递归锁
公众号:pythonislover
前面说到过python多线程的基本使用,大概的内容有几点
1.创建线程对象 t1 = threading.Thread(target=say,args=('tony',)) 2.启动线程 t1.start() 后面又说了两个点就是join和守护线程的概念
但是不知道大家有没有注意到一点就是前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,如果我们多个线程要用到相同的数据,那么就会存在资源争用和锁的问题,不管在什么语言中,这个都是不能避免的。对数据库属性的同学应该也了解,数据库中也存在锁的概念。
今天这篇文章我们说说python多线程中的同步锁,死锁和递归锁的使用。
- Python同步锁
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。
下面我们来举个例子说明如果多线程在没有同步锁的情况下访问公共资源会导致什么情况
import threading import time num = 100 def fun_sub(): global num # num -= 1 num2 = num time.sleep(0.001) num = num2-1 if __name__ == '__main__': print('开始测试同步锁 at %s' % time.ctime()) thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('结束测试同步锁 at %s' % time.ctime())
上面的例子其实很简单就是创建100的线程,然后每个线程去从公共资源num变量去执行减1操作,按照正常情况下面,等到代码执行结束,打印num变量,应该得到的是0,因为100个线程都去执行了一次减1的操作。
但是结果却不是我们想想的,我们看看结果
开始测试同步锁 at Sun Apr 28 09:56:45 2019 num is 91 结束测试同步锁 at Sun Apr 28 09:56:45 2019
我们会发现,每次执行的结果num值都不是一样的,上面显示的是91,那就存在问题了,为什么结果不是0呢?
我们来看看上面代码的执行流程。 1.因为GIL,只有一个线程(假设线程1)拿到了num这个资源,然后把变量赋值给num2,sleep 0.001秒,这时候num=100 2.当第一个线程sleep 0.001秒这个期间,这个线程会做yield操作,就是把cpu切换给别的线程执行(假设线程2拿到个GIL,获得cpu使用权),线程2也和线程1一样也拿到num,返回赋值给num2,然sleep,这时候,其实num还是=100. 3.线程2 sleep时候,又要yield操作,假设线程3拿到num,执行上面的操作,其实num有可能还是100 4.等到后面cpu重新切换给线程1,线程2,线程3上执行的时候,他们执行减1操作后,其实等到的num其实都是99,而不是顺序递减的。 5.其他剩余的线程操作如上
大家应该发现问题了,结果和我们想想的不一样,那我们怎么才能等到我们想要的结果呢?就是100个线程操作num变量得到最后结果为0?
这里就要借助于python的同步锁了,也就是同一时间只能放一个线程来操作num变量,减1之后,后面的线程操作来操作num变量。看看下面我们怎么实现。
import threading import time num = 100 def fun_sub(): global num lock.acquire() print('----加锁----') print('现在操作共享资源的线程名字是:',t.name) num2 = num time.sleep(0.001) num = num2-1 lock.release() print('----释放锁----') if __name__ == '__main__': print('开始测试同步锁 at %s' % time.ctime()) lock = threading.Lock() #创建一把同步锁 thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('结束测试同步锁 at %s' % time.ctime())
看到上面我们给中间的减1代码块,加个一把同步锁,这样,我们就可以得到我们想要的结果了,这就是同步锁的作用,一次只有一个线程操作同享资源。
看看上面代码执行的结果:
....... ----加锁---- 现在操作共享资源的线程名字是: Thread-98 ----释放锁---- ----加锁---- 现在操作共享资源的线程名字是: Thread-100 ----释放锁---- num is 0 结束测试同步锁 at Sun Apr 28 12:08:27 2019
- Python死锁
死锁的这个概念在很多地方都存在,比较在数据中,大概介绍下私有是怎么产生的
1.A拿了一个苹果
2.B拿了一个香蕉
3.A现在想再拿个香蕉,就在等待B释放这个香蕉
4.B同时想要再拿个苹果,这时候就等待A释放苹果
5.这样就是陷入了僵局,这就是生活中的死锁
python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:
import threading import time lock_apple = threading.Lock() lock_banana = threading.Lock() class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): lock_apple.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放 print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime())) lock_banana.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) lock_banana.release() lock_apple.release() def fun2(self): lock_banana.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) time.sleep(0.1) lock_apple.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime())) lock_apple.release() lock_banana.release() if __name__ == "__main__": for i in range(0, 10): #建立10个线程 my_thread = MyThread() #类继承法是python多线程的另外一种实现方式 my_thread.start() 代码执行hung住,死锁了 线程 Thread-1 , 想拿: 苹果--Sun Apr 28 12:21:06 2019 线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019 线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019 线程 Thread-2 , 想拿: 苹果--Sun Apr 28 12:21:06 2019 Process finished with exit code -1
上面的代码其实就是描述了苹果和香蕉的故事。大家可以仔细看看过程。下面我们看看执行流程
1.fun1中,线程1先拿了苹果,然后拿了香蕉,然后释放香蕉和苹果,然后再在fun2中又拿了香蕉,sleep 0.1秒。
2.在线程1的执行过程中,线程2进入了,因为苹果被线程1释放了,线程2这时候获得了苹果,然后想拿香蕉
3.这时候就出现问题了,线程一拿完香蕉之后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了
4.双向等待,出现死锁,代码执行不下去了
上面就是大概的执行流程和死锁出现的原因。在这种情况下就是在同一线程中多次请求同一资源时候出现的问题。
- Python递归锁RLock
为了支持在同一线程中多次请求同一资源,python提供了"递归锁":threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
下面我们用递归锁RLock解决上面的死锁问题
import threading import time lock = threading.RLock() #递归锁 class MyThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): lock.acquire() # 如果锁被占用,则阻塞在这里,等待锁的释放 print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime())) lock.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) lock.release() lock.release() def fun2(self): lock.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime())) time.sleep(0.1) lock.acquire() print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime())) lock.release() lock.release() if __name__ == "__main__": for i in range(0, 10): #建立10个线程 my_thread = MyThread() #类继承法是python多线程的另外一种实现方式 my_thread.start()
上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。
另外一点前面没有就算用类继承的方法实现python多线程,这个大家可以查下,就算继承Thread类,然后重新run方法来实现。
最后大家可能还有个疑问,就算如果我们都加锁了,也就是单线程了,那我们还要开多线程有什么用呢?这里解释下,在访问共享资源的时候,锁是一定要存在了,但是我们的代码中不是总是在访问公共资源的,还有一些其他的逻辑可以使用多线程,所以我们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。
好了今天就说到这里,个人意见,望指教。
文章推荐:
Python基础-不一样的切片操作 - https://mp.weixin.qq.com/s/3pute7-xrdhAt2Yk41nw9w
Python那些有用的小知识点1 - https://mp.weixin.qq.com/s/JXnBrYj0TEAo9LNrVv2cPA
Python日志模块之你还在用PRINT打印日志吗 - https://mp.weixin.qq.com/s/P_SDxmbxkK1ei3-sq5Qhlw
90%的人说Python程序慢,5大神招让你的代码像赛车一样跑起来 - https://mp.weixin.qq.com/s/XZcqzwodPjvjltPRNRTpVQ
110道python面试题 - https://mp.weixin.qq.com/s/SyC_LLQL8AU3i6wYNlOdNQ
Python你想知道的时间格式Time都在这里 - https://mp.weixin.qq.com/s/fXv9leS8xsKUEVRvo8sKdA
深度辨析 Python 的 eval() 与 exec() - https://mp.weixin.qq.com/s/Dkn6TXUSfEk_Lg4SF0m81g
为什么range不是迭代器?range到底是什么类型? - https://mp.weixin.qq.com/s/9FdWqAuQgv-t-yLJXwiHTg
6种方法让Python遍历多个列表 - https://mp.weixin.qq.com/s/z1wuXp8x08YZQMmKIMQHQg
Python之sorted,max,min内置函数用法 - https://mp.weixin.qq.com/s/KDRedycdz3qAGsaFMrqJBg
愉快地迁移到Python3 - https://mp.weixin.qq.com/s/RPdx2G36kDIQ3_G_XPk9tQ

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
一篇文章搞懂Python多线程简单实现和GIL
公众号:pythonislover 今天开始打算开一个新系列,就是python的多线程和多进程实现,这部分可能有些新手还是比较模糊的,都知道python中的多线程是假的,但是又不知道怎么回事,首先我们看一个例子来看看python多线程的实现。 import threading import time def say(name): print('你好%s at %s' %(name,time.ctime())) time.sleep(2) print("结束%s at %s" %(name,time.ctime())) def listen(name): print('你好%s at %s' % (name,time.ctime())) time.sleep(4) print("结束%s at %s" % (name,time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=say,args=('tony',)) #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1 t1.start...
- 下一篇
一篇文章理清Python多线程之同步条件,信号量和队列
公众号:pythonislover 今天这篇文章大概介绍下python多线程中的同步条件Event,信号量(Semaphore)和队列(queue),这是我们多线程系列的最后一篇文章,以后将会进入python多进程的系列。 同步条件(Event) 先说说为什么我们需要这个同步条件,我们的python多线程在执行task过程中,是相互竞争的,大家都可以先获取cpu的执行权限,这就是问题所在的地方,每个线程都是独立运行且状态不可预测,但是我们想想如果我们的业务中需要根据情况来决定线程的执行顺序,也就是程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时候我们就需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。 在 初始情况下,Event对象中的信号标志被设置为假,如果有线程等待一个Event对象, ,那么这个线程将会被一直阻塞直至该标志为真。 一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程继续执行。 Event的方法如下: event.isSet():返回eve...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- 2048小游戏-低调大师作品